From 388e22369ed9c564efd49e5e4086e5d33198a7af Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 2 Sep 2026 14:21:15 +0200 Subject: [PATCH 01/11] master: improve Copilot lifecycle labels --- action.yml | 40 +- build/cli/index.js | 338 ++++++++++++-- build/cli/src/actions/common_action.d.ts | 3 +- .../actions/local_action_configuration.d.ts | 4 +- .../local_action_configuration_sections.d.ts | 4 +- .../policies/agent_activity_label_policy.d.ts | 2 + .../policies/agent_activity_policy.d.ts | 4 + .../lifecycle_waiting_state_policy.d.ts | 18 + .../synchronize_agent_activity_use_case.d.ts | 16 + build/cli/src/domain/copilot_lifecycle.d.ts | 27 +- .../agent_activity_composition_root.d.ts | 2 + build/cli/src/utils/constants.d.ts | 18 +- build/github_action/index.js | 440 +++++++++++++++--- .../src/actions/common_action.d.ts | 3 +- .../actions/local_action_configuration.d.ts | 4 +- .../local_action_configuration_sections.d.ts | 4 +- .../policies/agent_activity_label_policy.d.ts | 2 + .../policies/agent_activity_policy.d.ts | 4 + .../lifecycle_waiting_state_policy.d.ts | 18 + .../synchronize_agent_activity_use_case.d.ts | 16 + .../src/domain/copilot_lifecycle.d.ts | 27 +- .../agent_activity_composition_root.d.ts | 2 + build/github_action/src/utils/constants.d.ts | 18 +- docs/features.mdx | 2 +- docs/how-to-use.mdx | 21 +- docs/issues/index.mdx | 35 +- docs/pull-requests/capabilities.mdx | 2 +- docs/pull-requests/workflow-setup.mdx | 2 +- src/actions/__tests__/common_action.test.ts | 36 ++ src/actions/common_action.ts | 37 +- src/actions/github_action.ts | 2 + src/actions/github_action_label_inputs.ts | 18 +- src/actions/local_action.ts | 9 +- .../local_action_configuration_sections.ts | 18 +- .../agent_activity_label_policy.test.ts | 27 ++ .../__tests__/agent_activity_policy.test.ts | 105 +++++ .../initial_label_provisioning_policy.test.ts | 27 +- .../__tests__/lifecycle_state_policy.test.ts | 4 +- .../lifecycle_waiting_state_policy.test.ts | 27 ++ .../policies/agent_activity_label_policy.ts | 12 + .../policies/agent_activity_policy.ts | 65 +++ .../initial_label_provisioning_policy.ts | 4 +- .../policies/lifecycle_state_policy.ts | 1 - .../lifecycle_waiting_state_policy.ts | 42 ++ ...ynchronize_agent_activity_use_case.test.ts | 101 ++++ ...nchronize_lifecycle_state_use_case.test.ts | 121 ++++- .../synchronize_agent_activity_use_case.ts | 92 ++++ .../synchronize_lifecycle_state_use_case.ts | 78 +++- ...ssue_label_provisioning_repository.test.ts | 14 +- .../__tests__/copilot_lifecycle.test.ts | 19 +- src/domain/copilot_lifecycle.ts | 142 +++++- .../agent_activity_composition_root.ts | 6 + src/utils/constants.ts | 20 +- 53 files changed, 1843 insertions(+), 260 deletions(-) create mode 100644 build/cli/src/application/policies/agent_activity_label_policy.d.ts create mode 100644 build/cli/src/application/policies/agent_activity_policy.d.ts create mode 100644 build/cli/src/application/policies/lifecycle_waiting_state_policy.d.ts create mode 100644 build/cli/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts create mode 100644 build/cli/src/infrastructure/composition/agent_activity_composition_root.d.ts create mode 100644 build/github_action/src/application/policies/agent_activity_label_policy.d.ts create mode 100644 build/github_action/src/application/policies/agent_activity_policy.d.ts create mode 100644 build/github_action/src/application/policies/lifecycle_waiting_state_policy.d.ts create mode 100644 build/github_action/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts create mode 100644 build/github_action/src/infrastructure/composition/agent_activity_composition_root.d.ts create mode 100644 src/application/policies/__tests__/agent_activity_label_policy.test.ts create mode 100644 src/application/policies/__tests__/agent_activity_policy.test.ts create mode 100644 src/application/policies/__tests__/lifecycle_waiting_state_policy.test.ts create mode 100644 src/application/policies/agent_activity_label_policy.ts create mode 100644 src/application/policies/agent_activity_policy.ts create mode 100644 src/application/policies/lifecycle_waiting_state_policy.ts create mode 100644 src/application/usecases/actions/__tests__/synchronize_agent_activity_use_case.test.ts create mode 100644 src/application/usecases/actions/synchronize_agent_activity_use_case.ts create mode 100644 src/infrastructure/composition/agent_activity_composition_root.ts diff --git a/action.yml b/action.yml index cc4b8889..fd758955 100644 --- a/action.yml +++ b/action.yml @@ -104,30 +104,36 @@ inputs: size-xs-label: description: "Label to indicate a task of size XS." default: "size: XS" - copilot-state-analyzing-label: - description: "Label for the Copilot lifecycle state: analyzing." - default: "copilot:state:analyzing" - copilot-state-planned-label: + state-ai-processing-label: + description: "Temporary label while a Copilot agent is analyzing or working." + default: "state:ai-processing" + state-planned-label: description: "Label for the Copilot lifecycle state: planned." - default: "copilot:state:planned" - copilot-state-in-progress-label: + default: "state:planned" + state-in-progress-label: description: "Label for the Copilot lifecycle state: implementation in progress." - default: "copilot:state:in-progress" - copilot-state-reviewing-label: + default: "state:in-progress" + state-reviewing-label: description: "Label for the Copilot lifecycle state: reviewing." - default: "copilot:state:reviewing" - copilot-state-changes-requested-label: + default: "state:reviewing" + state-changes-requested-label: description: "Label for the Copilot lifecycle state: changes requested." - default: "copilot:state:changes-requested" - copilot-state-verified-label: + default: "state:changes-requested" + state-verified-label: description: "Label for the Copilot lifecycle state: verified." - default: "copilot:state:verified" - copilot-state-ready-label: + default: "state:verified" + state-ready-label: description: "Label for the Copilot lifecycle state: ready." - default: "copilot:state:ready" - copilot-state-blocked-label: + default: "state:ready" + state-blocked-label: description: "Label for the Copilot lifecycle state: blocked." - default: "copilot:state:blocked" + default: "state:blocked" + state-awaiting-maintainer-label: + description: "Label for work waiting on a maintainer response or approval." + default: "state:awaiting-maintainer" + state-awaiting-issue-author-label: + description: "Label for work waiting on more information or changes from the issue author." + default: "state:awaiting-issue-author" size-xxl-threshold-lines: description: "Threshold for size XXL in lines." default: "1000" diff --git a/build/cli/index.js b/build/cli/index.js index 05e3e19c..079bb77c 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -52553,8 +52553,9 @@ const main_run_route_composition_root_1 = __nccwpck_require__(4706); const repository_context_1 = __nccwpck_require__(78958); const logging_ports_1 = __nccwpck_require__(6152); const logger_adapter_1 = __nccwpck_require__(72762); +const agent_activity_policy_1 = __nccwpck_require__(15375); const main_run_lifecycle_1 = __nccwpck_require__(916); -async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, lifecycleStateUseCase) { +async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, lifecycleStateUseCase, agentActivityUseCase) { (0, logging_ports_1.configureApplicationLogger)((0, logger_adapter_1.createLoggerAdapter)()); const repository = (0, repository_context_1.requireRepositoryCoordinates)({ owner: execution.owner, @@ -52571,10 +52572,10 @@ async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, l (0, logger_1.logDebugInfo)(`Setup done. Issue number: ${execution.issueNumber}, isSingleAction: ${execution.isSingleAction}, isIssue: ${execution.isIssue}, isPullRequest: ${execution.isPullRequest}, isPush: ${execution.isPush}`); const routeHandlers = (0, main_run_route_composition_root_1.createMainRunRouteCompositionRoot)(projectBoardCommandPort); if (execution.runnedByToken) { - return (0, main_run_lifecycle_1.runTokenExecution)(execution, routeHandlers); + return runTrackedRoute(execution, 'single-action', () => (0, main_run_lifecycle_1.runTokenExecution)(execution, routeHandlers), undefined, agentActivityUseCase); } if (execution.issueNumber === -1) { - return (0, main_run_lifecycle_1.runNoIssueExecution)(execution, routeHandlers); + return runTrackedRoute(execution, 'single-action', () => (0, main_run_lifecycle_1.runNoIssueExecution)(execution, routeHandlers), undefined, agentActivityUseCase); } (0, main_run_lifecycle_1.logWelcomeMessage)(execution); const route = (0, main_run_route_1.resolveMainRunRoute)({ @@ -52585,10 +52586,24 @@ async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, l isPullRequestReviewComment: execution.pullRequest.isPullRequestReviewComment, isPush: execution.isPush, }); - const results = await (0, main_run_lifecycle_1.runMainRoute)(execution, route, routeHandlers); - if (!lifecycleStateUseCase) - return results; - return [...results, ...(await lifecycleStateUseCase.invoke({ execution, results }))]; + if (route === 'unhandled') + return (0, main_run_lifecycle_1.runMainRoute)(execution, route, routeHandlers); + return runTrackedRoute(execution, route, () => (0, main_run_lifecycle_1.runMainRoute)(execution, route, routeHandlers), lifecycleStateUseCase, agentActivityUseCase); +} +async function runTrackedRoute(execution, route, run, lifecycleStateUseCase, agentActivityUseCase) { + const trackActivity = agentActivityUseCase !== undefined && (0, agent_activity_policy_1.shouldTrackAgentActivity)(execution, route); + if (trackActivity) + await agentActivityUseCase.start(execution); + try { + const results = await run(); + if (!lifecycleStateUseCase) + return results; + return [...results, ...(await lifecycleStateUseCase.invoke({ execution, results }))]; + } + finally { + if (trackActivity) + await agentActivityUseCase.finish(execution); + } } @@ -52813,13 +52828,14 @@ const local_action_output_1 = __nccwpck_require__(94290); const local_action_configuration_1 = __nccwpck_require__(66645); const local_action_execution_1 = __nccwpck_require__(47047); const repository_context_1 = __nccwpck_require__(78958); +const agent_activity_composition_root_1 = __nccwpck_require__(94253); async function runLocalAction(additionalParams) { const repository = (0, repository_context_1.requireRepositoryCoordinates)(additionalParams?.repo); const normalizedParams = { ...(additionalParams ?? {}), repo: repository }; const composition = (0, local_action_composition_root_1.createLocalActionCompositionRoot)(); const configuration = await (0, local_action_configuration_1.buildLocalActionConfiguration)(normalizedParams, composition.projectBoard.query); const execution = (0, local_action_execution_1.buildLocalActionExecution)(configuration, normalizedParams); - const results = await (0, common_action_1.mainRun)(execution, composition.projectBoard.command, composition.latestTagQuery); + const results = await (0, common_action_1.mainRun)(execution, composition.projectBoard.command, composition.latestTagQuery, undefined, (0, agent_activity_composition_root_1.createSynchronizeAgentActivityUseCase)()); (0, local_action_output_1.renderLocalActionResults)(results); } @@ -52977,14 +52993,16 @@ function readLocalLabelsAndIssueTypes(additionalParams, actionInputs) { sizeSLabel: label(constants_1.INPUT_KEYS.SIZE_S_LABEL), sizeXsLabel: label(constants_1.INPUT_KEYS.SIZE_XS_LABEL), lifecycle: { - analyzing: label(constants_1.INPUT_KEYS.COPILOT_STATE_ANALYZING_LABEL), - planned: label(constants_1.INPUT_KEYS.COPILOT_STATE_PLANNED_LABEL), - inProgress: label(constants_1.INPUT_KEYS.COPILOT_STATE_IN_PROGRESS_LABEL), - reviewing: label(constants_1.INPUT_KEYS.COPILOT_STATE_REVIEWING_LABEL), - changesRequested: label(constants_1.INPUT_KEYS.COPILOT_STATE_CHANGES_REQUESTED_LABEL), - verified: label(constants_1.INPUT_KEYS.COPILOT_STATE_VERIFIED_LABEL), - ready: label(constants_1.INPUT_KEYS.COPILOT_STATE_READY_LABEL), - blocked: label(constants_1.INPUT_KEYS.COPILOT_STATE_BLOCKED_LABEL), + aiProcessing: label(constants_1.INPUT_KEYS.STATE_AI_PROCESSING_LABEL), + planned: label(constants_1.INPUT_KEYS.STATE_PLANNED_LABEL), + inProgress: label(constants_1.INPUT_KEYS.STATE_IN_PROGRESS_LABEL), + reviewing: label(constants_1.INPUT_KEYS.STATE_REVIEWING_LABEL), + changesRequested: label(constants_1.INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), + verified: label(constants_1.INPUT_KEYS.STATE_VERIFIED_LABEL), + ready: label(constants_1.INPUT_KEYS.STATE_READY_LABEL), + blocked: label(constants_1.INPUT_KEYS.STATE_BLOCKED_LABEL), + awaitingMaintainer: label(constants_1.INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), + awaitingIssueAuthor: label(constants_1.INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), }, }, issueTypes: { @@ -53537,6 +53555,84 @@ function resolveWorkflowIdentifier(workflowRef) { } +/***/ }), + +/***/ 79966: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.replaceAgentActivityLabel = replaceAgentActivityLabel; +/** Adds or removes one activity label without touching unrelated labels. */ +function replaceAgentActivityLabel(currentLabels, activityLabel, active) { + const normalizedActivityLabel = activityLabel.trim().toLowerCase(); + if (!normalizedActivityLabel) + return [...currentLabels]; + const retained = currentLabels.filter(label => label.trim().toLowerCase() !== normalizedActivityLabel); + return active ? [...retained, activityLabel] : retained; +} + + +/***/ }), + +/***/ 15375: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.shouldTrackAgentActivity = shouldTrackAgentActivity; +const agent_1 = __nccwpck_require__(89040); +/** Decides whether a route can invoke an agent for its current event. */ +function shouldTrackAgentActivity(execution, route) { + if (!hasTarget(execution)) + return false; + switch (route) { + case 'issue': + return (execution.issue.opened || execution.issue.descriptionEdited) + && isAgentReady(execution, 'planner'); + case 'issue-comment': + case 'pull-request-review-comment': + return hasComment(execution) + && (isAgentReady(execution, 'planner') + || isAgentReady(execution, 'findings') + || isAgentReady(execution, 'fixer')); + case 'pull-request': + return ['opened', 'reopened', 'edited', 'synchronize'].includes(execution.pullRequest.action) + && (isAgentReady(execution, 'reviewer') + || (execution.ai.getAiPullRequestDescription() && isAgentReady(execution, 'planner'))); + case 'push': + return execution.commit.commits.length > 0 && isAgentReady(execution, 'findings'); + case 'single-action': + return isAgentBackedSingleAction(execution); + default: + return false; + } +} +function isAgentBackedSingleAction(execution) { + if (execution.singleAction.isThinkAction || execution.singleAction.isRecommendStepsAction) { + return isAgentReady(execution, 'planner'); + } + if (execution.singleAction.isCheckProgressAction || execution.singleAction.isDetectPotentialProblemsAction) { + return isAgentReady(execution, 'findings'); + } + return false; +} +function isAgentReady(execution, task) { + return (0, agent_1.isAgentConfigurationReady)(execution.ai?.getAgentConfiguration(task)); +} +function hasComment(execution) { + return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; +} +function hasTarget(execution) { + if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + return execution.pullRequest.number > 0; + } + return execution.issue.number > 0 || execution.issueNumber > 0; +} + + /***/ }), /***/ 15044: @@ -54331,7 +54427,7 @@ function progressLabelDefinitions() { })); } function lifecycleLabelDefinitionsFor(labels) { - return (0, copilot_lifecycle_1.lifecycleLabelDefinitions)(labels.lifecycle).map(definition => ({ + return (0, copilot_lifecycle_1.managedLifecycleLabelDefinitions)(labels.lifecycle).map(definition => ({ name: definition.name, color: definition.color, description: definition.description, @@ -55827,6 +55923,87 @@ async function syncProgressLabelsToOpenPullRequests(owner, repo, branch, progres } +/***/ }), + +/***/ 44880: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SynchronizeAgentActivityUseCase = void 0; +const copilot_lifecycle_1 = __nccwpck_require__(72418); +const agent_activity_label_policy_1 = __nccwpck_require__(79966); +const logging_ports_1 = __nccwpck_require__(6152); +/** + * Maintains the temporary agent-activity label around a complete route. + * Cleanup is deliberately best-effort so a label outage never hides the + * actual route result; the in-memory execution remains synchronized after a + * successful mutation so later lifecycle writes preserve the activity label. + */ +class SynchronizeAgentActivityUseCase { + constructor(issueLabelsPort) { + this.issueLabelsPort = issueLabelsPort; + this.taskId = 'SynchronizeAgentActivityUseCase'; + } + async start(execution) { + await this.synchronize(execution, true); + } + async finish(execution) { + await this.synchronize(execution, false); + } + async synchronize(execution, active) { + const target = resolveTarget(execution); + if (!target) { + (0, logging_ports_1.logDebugInfo)(`${this.taskId}: no issue or pull request target; skipping activity label.`); + return; + } + try { + // Route steps may have changed labels through their own ports. Read + // the latest server inventory before cleanup so removing the + // transient marker cannot overwrite those changes. + const currentLabels = active + ? target.labels + : await this.issueLabelsPort.getLabels(execution.owner, execution.repo, target.number, execution.tokens.token); + const configuredLabel = (0, copilot_lifecycle_1.activityLabel)(execution.labels.lifecycle); + const nextLabels = (0, agent_activity_label_policy_1.replaceAgentActivityLabel)(currentLabels, configuredLabel, active); + if (sameLabels(currentLabels, nextLabels)) + return; + await this.issueLabelsPort.setLabels(execution.owner, execution.repo, target.number, nextLabels, execution.tokens.token); + target.setLabels(nextLabels); + (0, logging_ports_1.logInfo)(`${active ? 'Added' : 'Removed'} Copilot agent activity label on target #${target.number}.`); + } + catch (error) { + const message = `${this.taskId}: unable to ${active ? 'add' : 'remove'} agent activity label.`; + (0, logging_ports_1.logError)(message, error instanceof Error ? { stack: error.stack } : undefined); + } + } +} +exports.SynchronizeAgentActivityUseCase = SynchronizeAgentActivityUseCase; +function resolveTarget(execution) { + if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + if (execution.pullRequest.number <= 0) + return undefined; + return { + number: execution.pullRequest.number, + labels: execution.labels.currentPullRequestLabels, + setLabels: labels => { execution.labels.currentPullRequestLabels = labels; }, + }; + } + const number = execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; + if (number <= 0) + return undefined; + return { + number, + labels: execution.labels.currentIssueLabels, + setLabels: labels => { execution.labels.currentIssueLabels = labels; }, + }; +} +function sameLabels(left, right) { + return left.length === right.length && left.every((label, index) => label === right[index]); +} + + /***/ }), /***/ 42442: @@ -70113,21 +70290,30 @@ function parseCopilotCommand(raw) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.DEFAULT_COPILOT_LIFECYCLE_LABELS = void 0; exports.lifecycleLabelDefinitions = lifecycleLabelDefinitions; +exports.activityLabelDefinitions = activityLabelDefinitions; +exports.waitingLabelDefinitions = waitingLabelDefinitions; +exports.managedLifecycleLabelDefinitions = managedLifecycleLabelDefinitions; exports.lifecycleLabelNames = lifecycleLabelNames; +exports.activityLabelNames = activityLabelNames; +exports.waitingLabelNames = waitingLabelNames; +exports.managedLifecycleLabelNames = managedLifecycleLabelNames; exports.lifecycleStateLabel = lifecycleStateLabel; +exports.activityLabel = activityLabel; +exports.waitingStateLabel = waitingStateLabel; exports.lifecycleStateFromLabels = lifecycleStateFromLabels; exports.DEFAULT_COPILOT_LIFECYCLE_LABELS = { - analyzing: 'copilot:state:analyzing', - planned: 'copilot:state:planned', - inProgress: 'copilot:state:in-progress', - reviewing: 'copilot:state:reviewing', - changesRequested: 'copilot:state:changes-requested', - verified: 'copilot:state:verified', - ready: 'copilot:state:ready', - blocked: 'copilot:state:blocked', + aiProcessing: 'state:ai-processing', + planned: 'state:planned', + inProgress: 'state:in-progress', + reviewing: 'state:reviewing', + changesRequested: 'state:changes-requested', + verified: 'state:verified', + ready: 'state:ready', + blocked: 'state:blocked', + awaitingMaintainer: 'state:awaiting-maintainer', + awaitingIssueAuthor: 'state:awaiting-issue-author', }; -const LIFECYCLE_METADATA = [ - ['analyzing', 'analyzing', 'FBCA04', 'Copilot is analyzing the issue or change.'], +const STABLE_LIFECYCLE_METADATA = [ ['planned', 'planned', '1D76DB', 'Copilot has produced an implementation plan.'], ['in-progress', 'inProgress', '0E8A16', 'Implementation work is in progress.'], ['reviewing', 'reviewing', '5319E7', 'A pull request is being reviewed.'], @@ -70136,23 +70322,81 @@ const LIFECYCLE_METADATA = [ ['ready', 'ready', '6F42C1', 'The change is ready for human approval or merge.'], ['blocked', 'blocked', 'B60205', 'The workflow is blocked and needs human input.'], ]; -function lifecycleLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { - return LIFECYCLE_METADATA.map(([state, key, color, description]) => ({ +const ACTIVITY_METADATA = [ + ['ai-processing', 'aiProcessing', 'FBCA04', 'A Copilot agent is analyzing or working on the issue or change.'], +]; +const WAITING_METADATA = [ + ['awaiting-maintainer', 'awaitingMaintainer', '5319E7', 'The next action requires a maintainer response or approval.'], + ['awaiting-issue-author', 'awaitingIssueAuthor', 'D93F0B', 'The next action requires more information or changes from the issue author.'], +]; +function stableDefinitions(labels) { + return STABLE_LIFECYCLE_METADATA.map(([state, key, color, description]) => ({ + category: 'lifecycle', state, name: labels[key], color, description, })); } +function activityDefinitions(labels) { + return ACTIVITY_METADATA.map(([, key, color, description]) => ({ + category: 'activity', + name: labels[key], + color, + description, + })); +} +function waitingDefinitions(labels) { + return WAITING_METADATA.map(([, key, color, description]) => ({ + category: 'waiting', + name: labels[key], + color, + description, + })); +} +function lifecycleLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return stableDefinitions(labels); +} +function activityLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return activityDefinitions(labels); +} +function waitingLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return waitingDefinitions(labels); +} +function managedLifecycleLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return [ + ...stableDefinitions(labels), + ...activityDefinitions(labels), + ...waitingDefinitions(labels), + ]; +} function lifecycleLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { return lifecycleLabelDefinitions(labels).map(definition => definition.name); } +function activityLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return activityLabelDefinitions(labels).map(definition => definition.name); +} +function waitingLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return waitingLabelDefinitions(labels).map(definition => definition.name); +} +function managedLifecycleLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return managedLifecycleLabelDefinitions(labels).map(definition => definition.name); +} function lifecycleStateLabel(state, labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { const definition = lifecycleLabelDefinitions(labels).find(candidate => candidate.state === state); if (!definition) throw new Error(`Unknown Copilot lifecycle state: ${state}`); return definition.name; } +function activityLabel(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return labels.aiProcessing; +} +function waitingStateLabel(state, labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + const metadata = WAITING_METADATA.find(([metadataState]) => metadataState === state); + if (!metadata) + throw new Error(`Unknown Copilot waiting state: ${state}`); + return labels[metadata[1]]; +} function lifecycleStateFromLabels(currentLabels, labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { const normalized = new Set(currentLabels.map(label => label.trim().toLowerCase())); return lifecycleLabelDefinitions(labels).find(definition => normalized.has(definition.name.trim().toLowerCase()))?.state; @@ -70365,6 +70609,22 @@ function createActorAuthorizationRepository() { } +/***/ }), + +/***/ 94253: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSynchronizeAgentActivityUseCase = createSynchronizeAgentActivityUseCase; +const synchronize_agent_activity_use_case_1 = __nccwpck_require__(44880); +const issue_labels_composition_root_1 = __nccwpck_require__(34780); +function createSynchronizeAgentActivityUseCase() { + return new synchronize_agent_activity_use_case_1.SynchronizeAgentActivityUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)()); +} + + /***/ }), /***/ 85079: @@ -72838,15 +73098,17 @@ exports.INPUT_KEYS = { SIZE_M_LABEL: 'size-m-label', SIZE_S_LABEL: 'size-s-label', SIZE_XS_LABEL: 'size-xs-label', - // Copilot lifecycle labels - COPILOT_STATE_ANALYZING_LABEL: 'copilot-state-analyzing-label', - COPILOT_STATE_PLANNED_LABEL: 'copilot-state-planned-label', - COPILOT_STATE_IN_PROGRESS_LABEL: 'copilot-state-in-progress-label', - COPILOT_STATE_REVIEWING_LABEL: 'copilot-state-reviewing-label', - COPILOT_STATE_CHANGES_REQUESTED_LABEL: 'copilot-state-changes-requested-label', - COPILOT_STATE_VERIFIED_LABEL: 'copilot-state-verified-label', - COPILOT_STATE_READY_LABEL: 'copilot-state-ready-label', - COPILOT_STATE_BLOCKED_LABEL: 'copilot-state-blocked-label', + // Lifecycle label inputs + STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', + STATE_PLANNED_LABEL: 'state-planned-label', + STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', + STATE_REVIEWING_LABEL: 'state-reviewing-label', + STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', + STATE_VERIFIED_LABEL: 'state-verified-label', + STATE_READY_LABEL: 'state-ready-label', + STATE_BLOCKED_LABEL: 'state-blocked-label', + STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', + STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', // Issue Types ISSUE_TYPE_BUG: 'issue-type-bug', ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', diff --git a/build/cli/src/actions/common_action.d.ts b/build/cli/src/actions/common_action.d.ts index 5bc3b1db..5b51e419 100644 --- a/build/cli/src/actions/common_action.d.ts +++ b/build/cli/src/actions/common_action.d.ts @@ -3,4 +3,5 @@ import { Result } from '../data/model/result'; import { ProjectBoardCommandPort } from '../application/ports/project_board_command_ports'; import type { LatestTagQueryPort } from '../application/ports/branch_tag_ports'; import type { SynchronizeLifecycleStateUseCase } from '../application/usecases/actions/synchronize_lifecycle_state_use_case'; -export declare function mainRun(execution: Execution, projectBoardCommandPort: ProjectBoardCommandPort, latestTagQueryPort: LatestTagQueryPort, lifecycleStateUseCase?: SynchronizeLifecycleStateUseCase): Promise; +import type { SynchronizeAgentActivityUseCase } from '../application/usecases/actions/synchronize_agent_activity_use_case'; +export declare function mainRun(execution: Execution, projectBoardCommandPort: ProjectBoardCommandPort, latestTagQueryPort: LatestTagQueryPort, lifecycleStateUseCase?: SynchronizeLifecycleStateUseCase, agentActivityUseCase?: SynchronizeAgentActivityUseCase): Promise; diff --git a/build/cli/src/actions/local_action_configuration.d.ts b/build/cli/src/actions/local_action_configuration.d.ts index aca66b8f..748e9222 100644 --- a/build/cli/src/actions/local_action_configuration.d.ts +++ b/build/cli/src/actions/local_action_configuration.d.ts @@ -94,7 +94,7 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn sizeSLabel: string; sizeXsLabel: string; lifecycle: { - analyzing: string; + aiProcessing: string; planned: string; inProgress: string; reviewing: string; @@ -102,6 +102,8 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn verified: string; ready: string; blocked: string; + awaitingMaintainer: string; + awaitingIssueAuthor: string; }; projectIdsInput: string; projectIds: string[]; diff --git a/build/cli/src/actions/local_action_configuration_sections.d.ts b/build/cli/src/actions/local_action_configuration_sections.d.ts index fc09d584..740ed554 100644 --- a/build/cli/src/actions/local_action_configuration_sections.d.ts +++ b/build/cli/src/actions/local_action_configuration_sections.d.ts @@ -65,7 +65,7 @@ export declare function readLocalLabelsAndIssueTypes(additionalParams: ActionInp sizeSLabel: string; sizeXsLabel: string; lifecycle: { - analyzing: string; + aiProcessing: string; planned: string; inProgress: string; reviewing: string; @@ -73,6 +73,8 @@ export declare function readLocalLabelsAndIssueTypes(additionalParams: ActionInp verified: string; ready: string; blocked: string; + awaitingMaintainer: string; + awaitingIssueAuthor: string; }; }; issueTypes: { diff --git a/build/cli/src/application/policies/agent_activity_label_policy.d.ts b/build/cli/src/application/policies/agent_activity_label_policy.d.ts new file mode 100644 index 00000000..9bfa5fa6 --- /dev/null +++ b/build/cli/src/application/policies/agent_activity_label_policy.d.ts @@ -0,0 +1,2 @@ +/** Adds or removes one activity label without touching unrelated labels. */ +export declare function replaceAgentActivityLabel(currentLabels: readonly string[], activityLabel: string, active: boolean): string[]; diff --git a/build/cli/src/application/policies/agent_activity_policy.d.ts b/build/cli/src/application/policies/agent_activity_policy.d.ts new file mode 100644 index 00000000..b26fe59f --- /dev/null +++ b/build/cli/src/application/policies/agent_activity_policy.d.ts @@ -0,0 +1,4 @@ +import type { Execution } from '../../data/model/execution'; +export type AgentActivityRoute = 'single-action' | 'issue-comment' | 'issue' | 'pull-request-review-comment' | 'pull-request' | 'push'; +/** Decides whether a route can invoke an agent for its current event. */ +export declare function shouldTrackAgentActivity(execution: Execution, route: AgentActivityRoute): boolean; diff --git a/build/cli/src/application/policies/lifecycle_waiting_state_policy.d.ts b/build/cli/src/application/policies/lifecycle_waiting_state_policy.d.ts new file mode 100644 index 00000000..a5d3091f --- /dev/null +++ b/build/cli/src/application/policies/lifecycle_waiting_state_policy.d.ts @@ -0,0 +1,18 @@ +import type { CopilotLifecycleState, CopilotWaitingState } from '../../domain/copilot_lifecycle'; +export type LifecycleWaitingStateDecision = { + kind: 'set'; + state: CopilotWaitingState; +} | { + kind: 'clear'; +} | { + kind: 'preserve'; +}; +export interface LifecycleWaitingStateInput { + readonly eventName: string; + readonly lifecycleState: CopilotLifecycleState | undefined; +} +/** + * Resolves who should provide the next human input. Waiting labels are + * orthogonal to the stable lifecycle phase and at most one is retained. + */ +export declare function resolveLifecycleWaitingState(input: LifecycleWaitingStateInput): LifecycleWaitingStateDecision; diff --git a/build/cli/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts b/build/cli/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts new file mode 100644 index 00000000..5b4fb8ca --- /dev/null +++ b/build/cli/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts @@ -0,0 +1,16 @@ +import type { Execution } from '../../../data/model/execution'; +import type { IssueLabelsPort } from '../../ports/issue_management_ports'; +/** + * Maintains the temporary agent-activity label around a complete route. + * Cleanup is deliberately best-effort so a label outage never hides the + * actual route result; the in-memory execution remains synchronized after a + * successful mutation so later lifecycle writes preserve the activity label. + */ +export declare class SynchronizeAgentActivityUseCase { + private readonly issueLabelsPort; + readonly taskId = "SynchronizeAgentActivityUseCase"; + constructor(issueLabelsPort: IssueLabelsPort); + start(execution: Execution): Promise; + finish(execution: Execution): Promise; + private synchronize; +} diff --git a/build/cli/src/domain/copilot_lifecycle.d.ts b/build/cli/src/domain/copilot_lifecycle.d.ts index ab840934..a9e5f070 100644 --- a/build/cli/src/domain/copilot_lifecycle.d.ts +++ b/build/cli/src/domain/copilot_lifecycle.d.ts @@ -1,11 +1,14 @@ /** - * The Copilot lifecycle is deliberately independent from GitHub's API model. - * Labels are the persistence representation; this policy is the state machine - * used by application workflows and can therefore be tested without I/O. + * Copilot labels are split into independent dimensions. A durable lifecycle + * phase can coexist with temporary agent activity and a human waiting state. + * This policy is independent from GitHub's API model and remains unit-testable + * without I/O. */ -export type CopilotLifecycleState = 'analyzing' | 'planned' | 'in-progress' | 'reviewing' | 'changes-requested' | 'verified' | 'ready' | 'blocked'; +export type CopilotLifecycleState = 'planned' | 'in-progress' | 'reviewing' | 'changes-requested' | 'verified' | 'ready' | 'blocked'; +export type CopilotAgentActivity = 'ai-processing'; +export type CopilotWaitingState = 'awaiting-maintainer' | 'awaiting-issue-author'; export interface CopilotLifecycleLabels { - analyzing: string; + aiProcessing: string; planned: string; inProgress: string; reviewing: string; @@ -13,15 +16,27 @@ export interface CopilotLifecycleLabels { verified: string; ready: string; blocked: string; + awaitingMaintainer: string; + awaitingIssueAuthor: string; } export declare const DEFAULT_COPILOT_LIFECYCLE_LABELS: Readonly; +export type LifecycleLabelCategory = 'lifecycle' | 'activity' | 'waiting'; export interface LifecycleLabelDefinition { - readonly state: CopilotLifecycleState; + readonly category: LifecycleLabelCategory; + readonly state?: CopilotLifecycleState; readonly name: string; readonly color: string; readonly description: string; } export declare function lifecycleLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; +export declare function activityLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; +export declare function waitingLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; +export declare function managedLifecycleLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; export declare function lifecycleLabelNames(labels?: CopilotLifecycleLabels): string[]; +export declare function activityLabelNames(labels?: CopilotLifecycleLabels): string[]; +export declare function waitingLabelNames(labels?: CopilotLifecycleLabels): string[]; +export declare function managedLifecycleLabelNames(labels?: CopilotLifecycleLabels): string[]; export declare function lifecycleStateLabel(state: CopilotLifecycleState, labels?: CopilotLifecycleLabels): string; +export declare function activityLabel(labels?: CopilotLifecycleLabels): string; +export declare function waitingStateLabel(state: CopilotWaitingState, labels?: CopilotLifecycleLabels): string; export declare function lifecycleStateFromLabels(currentLabels: readonly string[], labels?: CopilotLifecycleLabels): CopilotLifecycleState | undefined; diff --git a/build/cli/src/infrastructure/composition/agent_activity_composition_root.d.ts b/build/cli/src/infrastructure/composition/agent_activity_composition_root.d.ts new file mode 100644 index 00000000..4ed82efb --- /dev/null +++ b/build/cli/src/infrastructure/composition/agent_activity_composition_root.d.ts @@ -0,0 +1,2 @@ +import { SynchronizeAgentActivityUseCase } from '../../application/usecases/actions/synchronize_agent_activity_use_case'; +export declare function createSynchronizeAgentActivityUseCase(): SynchronizeAgentActivityUseCase; diff --git a/build/cli/src/utils/constants.d.ts b/build/cli/src/utils/constants.d.ts index 8512680b..43f98677 100644 --- a/build/cli/src/utils/constants.d.ts +++ b/build/cli/src/utils/constants.d.ts @@ -154,14 +154,16 @@ export declare const INPUT_KEYS: { readonly SIZE_M_LABEL: "size-m-label"; readonly SIZE_S_LABEL: "size-s-label"; readonly SIZE_XS_LABEL: "size-xs-label"; - readonly COPILOT_STATE_ANALYZING_LABEL: "copilot-state-analyzing-label"; - readonly COPILOT_STATE_PLANNED_LABEL: "copilot-state-planned-label"; - readonly COPILOT_STATE_IN_PROGRESS_LABEL: "copilot-state-in-progress-label"; - readonly COPILOT_STATE_REVIEWING_LABEL: "copilot-state-reviewing-label"; - readonly COPILOT_STATE_CHANGES_REQUESTED_LABEL: "copilot-state-changes-requested-label"; - readonly COPILOT_STATE_VERIFIED_LABEL: "copilot-state-verified-label"; - readonly COPILOT_STATE_READY_LABEL: "copilot-state-ready-label"; - readonly COPILOT_STATE_BLOCKED_LABEL: "copilot-state-blocked-label"; + readonly STATE_AI_PROCESSING_LABEL: "state-ai-processing-label"; + readonly STATE_PLANNED_LABEL: "state-planned-label"; + readonly STATE_IN_PROGRESS_LABEL: "state-in-progress-label"; + readonly STATE_REVIEWING_LABEL: "state-reviewing-label"; + readonly STATE_CHANGES_REQUESTED_LABEL: "state-changes-requested-label"; + readonly STATE_VERIFIED_LABEL: "state-verified-label"; + readonly STATE_READY_LABEL: "state-ready-label"; + readonly STATE_BLOCKED_LABEL: "state-blocked-label"; + readonly STATE_AWAITING_MAINTAINER_LABEL: "state-awaiting-maintainer-label"; + readonly STATE_AWAITING_ISSUE_AUTHOR_LABEL: "state-awaiting-issue-author-label"; readonly ISSUE_TYPE_BUG: "issue-type-bug"; readonly ISSUE_TYPE_BUG_DESCRIPTION: "issue-type-bug-description"; readonly ISSUE_TYPE_BUG_COLOR: "issue-type-bug-color"; diff --git a/build/github_action/index.js b/build/github_action/index.js index 144b7bb3..48d3fac0 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -48075,8 +48075,9 @@ const main_run_route_composition_root_1 = __nccwpck_require__(4706); const repository_context_1 = __nccwpck_require__(78958); const logging_ports_1 = __nccwpck_require__(6152); const logger_adapter_1 = __nccwpck_require__(72762); +const agent_activity_policy_1 = __nccwpck_require__(15375); const main_run_lifecycle_1 = __nccwpck_require__(916); -async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, lifecycleStateUseCase) { +async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, lifecycleStateUseCase, agentActivityUseCase) { (0, logging_ports_1.configureApplicationLogger)((0, logger_adapter_1.createLoggerAdapter)()); const repository = (0, repository_context_1.requireRepositoryCoordinates)({ owner: execution.owner, @@ -48093,10 +48094,10 @@ async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, l (0, logger_1.logDebugInfo)(`Setup done. Issue number: ${execution.issueNumber}, isSingleAction: ${execution.isSingleAction}, isIssue: ${execution.isIssue}, isPullRequest: ${execution.isPullRequest}, isPush: ${execution.isPush}`); const routeHandlers = (0, main_run_route_composition_root_1.createMainRunRouteCompositionRoot)(projectBoardCommandPort); if (execution.runnedByToken) { - return (0, main_run_lifecycle_1.runTokenExecution)(execution, routeHandlers); + return runTrackedRoute(execution, 'single-action', () => (0, main_run_lifecycle_1.runTokenExecution)(execution, routeHandlers), undefined, agentActivityUseCase); } if (execution.issueNumber === -1) { - return (0, main_run_lifecycle_1.runNoIssueExecution)(execution, routeHandlers); + return runTrackedRoute(execution, 'single-action', () => (0, main_run_lifecycle_1.runNoIssueExecution)(execution, routeHandlers), undefined, agentActivityUseCase); } (0, main_run_lifecycle_1.logWelcomeMessage)(execution); const route = (0, main_run_route_1.resolveMainRunRoute)({ @@ -48107,10 +48108,24 @@ async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, l isPullRequestReviewComment: execution.pullRequest.isPullRequestReviewComment, isPush: execution.isPush, }); - const results = await (0, main_run_lifecycle_1.runMainRoute)(execution, route, routeHandlers); - if (!lifecycleStateUseCase) - return results; - return [...results, ...(await lifecycleStateUseCase.invoke({ execution, results }))]; + if (route === 'unhandled') + return (0, main_run_lifecycle_1.runMainRoute)(execution, route, routeHandlers); + return runTrackedRoute(execution, route, () => (0, main_run_lifecycle_1.runMainRoute)(execution, route, routeHandlers), lifecycleStateUseCase, agentActivityUseCase); +} +async function runTrackedRoute(execution, route, run, lifecycleStateUseCase, agentActivityUseCase) { + const trackActivity = agentActivityUseCase !== undefined && (0, agent_activity_policy_1.shouldTrackAgentActivity)(execution, route); + if (trackActivity) + await agentActivityUseCase.start(execution); + try { + const results = await run(); + if (!lifecycleStateUseCase) + return results; + return [...results, ...(await lifecycleStateUseCase.invoke({ execution, results }))]; + } + finally { + if (trackActivity) + await agentActivityUseCase.finish(execution); + } } @@ -48251,6 +48266,7 @@ const github_execution_admission_composition_root_1 = __nccwpck_require__(54954) const lifecycle_state_composition_root_1 = __nccwpck_require__(4673); const copilot_evidence_composition_root_1 = __nccwpck_require__(64686); const github_action_summary_composition_root_1 = __nccwpck_require__(75305); +const agent_activity_composition_root_1 = __nccwpck_require__(94253); async function runGitHubAction() { if ((0, input_boolean_policy_1.isEnabledInput)((0, github_action_input_1.getGithubActionInput)(constants_1.INPUT_KEYS.QUEUE_GATE_ONLY))) { await runQueueGateOnly(); @@ -48291,7 +48307,7 @@ async function runGitHubAction() { }); (0, logger_1.logDebugInfo)(`Execution built. Event will be resolved in mainRun. Single action: ${execution.singleAction.currentSingleAction ?? 'none'}, ` + `AI PR description: ${execution.ai.getAiPullRequestDescription()}, bugbot min severity: ${execution.ai.getBugbotMinSeverity()}.`); - const results = await (0, common_action_1.mainRun)(execution, projectBoard.command, new git_cli_repository_1.GitCliRepository(), (0, lifecycle_state_composition_root_1.createSynchronizeLifecycleStateUseCase)()); + const results = await (0, common_action_1.mainRun)(execution, projectBoard.command, new git_cli_repository_1.GitCliRepository(), (0, lifecycle_state_composition_root_1.createSynchronizeLifecycleStateUseCase)(), (0, agent_activity_composition_root_1.createSynchronizeAgentActivityUseCase)()); const issueContentPort = (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(); await (0, github_action_completion_1.finishGithubAction)(execution, results, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), new configuration_handler_1.ConfigurationHandler(issueContentPort), (0, copilot_evidence_composition_root_1.createCopilotEvidenceCompositionRoot)(), (0, github_action_summary_composition_root_1.createGithubActionSummaryCompositionRoot)()); } @@ -48735,14 +48751,16 @@ function readGithubActionLabelInputs(getInput) { s: getInput(constants_1.INPUT_KEYS.SIZE_S_LABEL), xs: getInput(constants_1.INPUT_KEYS.SIZE_XS_LABEL), }, lifecycle: { - analyzing: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_ANALYZING_LABEL), - planned: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_PLANNED_LABEL), - inProgress: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_IN_PROGRESS_LABEL), - reviewing: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_REVIEWING_LABEL), - changesRequested: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_CHANGES_REQUESTED_LABEL), - verified: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_VERIFIED_LABEL), - ready: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_READY_LABEL), - blocked: getInput(constants_1.INPUT_KEYS.COPILOT_STATE_BLOCKED_LABEL), + aiProcessing: getInput(constants_1.INPUT_KEYS.STATE_AI_PROCESSING_LABEL), + planned: getInput(constants_1.INPUT_KEYS.STATE_PLANNED_LABEL), + inProgress: getInput(constants_1.INPUT_KEYS.STATE_IN_PROGRESS_LABEL), + reviewing: getInput(constants_1.INPUT_KEYS.STATE_REVIEWING_LABEL), + changesRequested: getInput(constants_1.INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), + verified: getInput(constants_1.INPUT_KEYS.STATE_VERIFIED_LABEL), + ready: getInput(constants_1.INPUT_KEYS.STATE_READY_LABEL), + blocked: getInput(constants_1.INPUT_KEYS.STATE_BLOCKED_LABEL), + awaitingMaintainer: getInput(constants_1.INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), + awaitingIssueAuthor: getInput(constants_1.INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), }, }; } @@ -49457,6 +49475,84 @@ function escapeTable(value) { } +/***/ }), + +/***/ 79966: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.replaceAgentActivityLabel = replaceAgentActivityLabel; +/** Adds or removes one activity label without touching unrelated labels. */ +function replaceAgentActivityLabel(currentLabels, activityLabel, active) { + const normalizedActivityLabel = activityLabel.trim().toLowerCase(); + if (!normalizedActivityLabel) + return [...currentLabels]; + const retained = currentLabels.filter(label => label.trim().toLowerCase() !== normalizedActivityLabel); + return active ? [...retained, activityLabel] : retained; +} + + +/***/ }), + +/***/ 15375: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.shouldTrackAgentActivity = shouldTrackAgentActivity; +const agent_1 = __nccwpck_require__(89040); +/** Decides whether a route can invoke an agent for its current event. */ +function shouldTrackAgentActivity(execution, route) { + if (!hasTarget(execution)) + return false; + switch (route) { + case 'issue': + return (execution.issue.opened || execution.issue.descriptionEdited) + && isAgentReady(execution, 'planner'); + case 'issue-comment': + case 'pull-request-review-comment': + return hasComment(execution) + && (isAgentReady(execution, 'planner') + || isAgentReady(execution, 'findings') + || isAgentReady(execution, 'fixer')); + case 'pull-request': + return ['opened', 'reopened', 'edited', 'synchronize'].includes(execution.pullRequest.action) + && (isAgentReady(execution, 'reviewer') + || (execution.ai.getAiPullRequestDescription() && isAgentReady(execution, 'planner'))); + case 'push': + return execution.commit.commits.length > 0 && isAgentReady(execution, 'findings'); + case 'single-action': + return isAgentBackedSingleAction(execution); + default: + return false; + } +} +function isAgentBackedSingleAction(execution) { + if (execution.singleAction.isThinkAction || execution.singleAction.isRecommendStepsAction) { + return isAgentReady(execution, 'planner'); + } + if (execution.singleAction.isCheckProgressAction || execution.singleAction.isDetectPotentialProblemsAction) { + return isAgentReady(execution, 'findings'); + } + return false; +} +function isAgentReady(execution, task) { + return (0, agent_1.isAgentConfigurationReady)(execution.ai?.getAgentConfiguration(task)); +} +function hasComment(execution) { + return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; +} +function hasTarget(execution) { + if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + return execution.pullRequest.number > 0; + } + return execution.issue.number > 0 || execution.issueNumber > 0; +} + + /***/ }), /***/ 15044: @@ -50353,7 +50449,7 @@ function progressLabelDefinitions() { })); } function lifecycleLabelDefinitionsFor(labels) { - return (0, copilot_lifecycle_1.lifecycleLabelDefinitions)(labels.lifecycle).map(definition => ({ + return (0, copilot_lifecycle_1.managedLifecycleLabelDefinitions)(labels.lifecycle).map(definition => ({ name: definition.name, color: definition.color, description: definition.description, @@ -50424,8 +50520,6 @@ function resolveLifecycleState(input) { return 'planned'; if (hasExplicitPlanningCommand(input.results)) return 'planned'; - if (input.issueOpened || input.issueDescriptionEdited) - return 'analyzing'; return undefined; } function isFindingStateCounts(value) { @@ -50454,6 +50548,44 @@ function hasSuccessfulResult(results, id) { } +/***/ }), + +/***/ 61736: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.resolveLifecycleWaitingState = resolveLifecycleWaitingState; +/** + * Resolves who should provide the next human input. Waiting labels are + * orthogonal to the stable lifecycle phase and at most one is retained. + */ +function resolveLifecycleWaitingState(input) { + if (input.lifecycleState === 'planned' + || input.lifecycleState === 'ready' + || input.lifecycleState === 'blocked') { + return { kind: 'set', state: 'awaiting-maintainer' }; + } + if (input.lifecycleState === 'changes-requested') { + return { kind: 'set', state: 'awaiting-issue-author' }; + } + if (input.lifecycleState !== undefined || isHumanInteraction(input.eventName)) { + return { kind: 'clear' }; + } + return { kind: 'preserve' }; +} +function isHumanInteraction(eventName) { + return [ + 'issues', + 'issue_comment', + 'pull_request', + 'pull_request_review_comment', + 'push', + ].includes(eventName); +} + + /***/ }), /***/ 55078: @@ -52080,6 +52212,87 @@ async function syncProgressLabelsToOpenPullRequests(owner, repo, branch, progres } +/***/ }), + +/***/ 44880: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SynchronizeAgentActivityUseCase = void 0; +const copilot_lifecycle_1 = __nccwpck_require__(72418); +const agent_activity_label_policy_1 = __nccwpck_require__(79966); +const logging_ports_1 = __nccwpck_require__(6152); +/** + * Maintains the temporary agent-activity label around a complete route. + * Cleanup is deliberately best-effort so a label outage never hides the + * actual route result; the in-memory execution remains synchronized after a + * successful mutation so later lifecycle writes preserve the activity label. + */ +class SynchronizeAgentActivityUseCase { + constructor(issueLabelsPort) { + this.issueLabelsPort = issueLabelsPort; + this.taskId = 'SynchronizeAgentActivityUseCase'; + } + async start(execution) { + await this.synchronize(execution, true); + } + async finish(execution) { + await this.synchronize(execution, false); + } + async synchronize(execution, active) { + const target = resolveTarget(execution); + if (!target) { + (0, logging_ports_1.logDebugInfo)(`${this.taskId}: no issue or pull request target; skipping activity label.`); + return; + } + try { + // Route steps may have changed labels through their own ports. Read + // the latest server inventory before cleanup so removing the + // transient marker cannot overwrite those changes. + const currentLabels = active + ? target.labels + : await this.issueLabelsPort.getLabels(execution.owner, execution.repo, target.number, execution.tokens.token); + const configuredLabel = (0, copilot_lifecycle_1.activityLabel)(execution.labels.lifecycle); + const nextLabels = (0, agent_activity_label_policy_1.replaceAgentActivityLabel)(currentLabels, configuredLabel, active); + if (sameLabels(currentLabels, nextLabels)) + return; + await this.issueLabelsPort.setLabels(execution.owner, execution.repo, target.number, nextLabels, execution.tokens.token); + target.setLabels(nextLabels); + (0, logging_ports_1.logInfo)(`${active ? 'Added' : 'Removed'} Copilot agent activity label on target #${target.number}.`); + } + catch (error) { + const message = `${this.taskId}: unable to ${active ? 'add' : 'remove'} agent activity label.`; + (0, logging_ports_1.logError)(message, error instanceof Error ? { stack: error.stack } : undefined); + } + } +} +exports.SynchronizeAgentActivityUseCase = SynchronizeAgentActivityUseCase; +function resolveTarget(execution) { + if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + if (execution.pullRequest.number <= 0) + return undefined; + return { + number: execution.pullRequest.number, + labels: execution.labels.currentPullRequestLabels, + setLabels: labels => { execution.labels.currentPullRequestLabels = labels; }, + }; + } + const number = execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; + if (number <= 0) + return undefined; + return { + number, + labels: execution.labels.currentIssueLabels, + setLabels: labels => { execution.labels.currentIssueLabels = labels; }, + }; +} +function sameLabels(left, right) { + return left.length === right.length && left.every((label, index) => label === right[index]); +} + + /***/ }), /***/ 18032: @@ -52092,6 +52305,7 @@ exports.SynchronizeLifecycleStateUseCase = void 0; const result_1 = __nccwpck_require__(73817); const copilot_lifecycle_1 = __nccwpck_require__(72418); const lifecycle_state_policy_1 = __nccwpck_require__(34026); +const lifecycle_waiting_state_policy_1 = __nccwpck_require__(61736); const logging_ports_1 = __nccwpck_require__(6152); /** * Reconciles one state label after a route completes. The existing business @@ -52107,32 +52321,38 @@ class SynchronizeLifecycleStateUseCase { eventName: param.execution.eventName, action: param.execution.inputs?.action ?? '', isIssue: ['issues', 'issue_comment'].includes(param.execution.eventName), - isPullRequest: param.execution.eventName === 'pull_request', + isPullRequest: ['pull_request', 'pull_request_review_comment'].includes(param.execution.eventName), issueOpened: param.execution.issue.opened, issueDescriptionEdited: param.execution.issue.descriptionEdited, pullRequestMerged: param.execution.pullRequest.isMerged, pullRequestClosed: param.execution.pullRequest.isClosed, results: param.results, }); - if (!state) - return []; + const waitingDecision = (0, lifecycle_waiting_state_policy_1.resolveLifecycleWaitingState)({ + eventName: param.execution.eventName, + lifecycleState: state, + }); const issueNumber = targetNumber(param.execution); if (issueNumber <= 0) { (0, logging_ports_1.logDebugInfo)('Lifecycle state synchronization skipped: no issue or pull request number.'); return []; } - const currentLabels = targetLabels(param.execution); - const nextLabels = replaceLifecycleLabels(currentLabels, state, param.execution.labels.lifecycle); - if (sameLabels(currentLabels, nextLabels)) - return []; try { - await this.issueLabelsPort.setLabels(param.execution.owner, param.execution.repo, issueNumber, nextLabels, param.execution.tokens.token); - setTargetLabels(param.execution, nextLabels); + // Route steps may have changed labels through their own ports. Use + // the latest server inventory before reconciliation so this + // use case cannot overwrite those changes with setup-time data. + const currentLabels = await this.issueLabelsPort.getLabels(param.execution.owner, param.execution.repo, issueNumber, param.execution.tokens.token) ?? targetLabels(param.execution); + const nextLabels = replaceLifecycleLabels(currentLabels, state, param.execution.labels.lifecycle); + const nextLabelsWithWaiting = replaceWaitingLabels(nextLabels, waitingDecision, param.execution.labels.lifecycle); + if (sameLabels(currentLabels, nextLabelsWithWaiting)) + return []; + await this.issueLabelsPort.setLabels(param.execution.owner, param.execution.repo, issueNumber, nextLabelsWithWaiting, param.execution.tokens.token); + setTargetLabels(param.execution, nextLabelsWithWaiting); return [new result_1.Result({ id: this.taskId, success: true, executed: true, - steps: [`Lifecycle state synchronized to \`${state}\`.`], + steps: lifecycleSynchronizationSteps(state, waitingDecision), })]; } catch (error) { @@ -52144,29 +52364,54 @@ class SynchronizeLifecycleStateUseCase { } exports.SynchronizeLifecycleStateUseCase = SynchronizeLifecycleStateUseCase; function targetNumber(execution) { - if (['issues', 'issue_comment'].includes(execution.eventName)) - return execution.issue.number; - if (execution.eventName === 'pull_request') + if (['issues', 'issue_comment', 'push'].includes(execution.eventName)) { + return execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; + } + if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) return execution.pullRequest.number; return -1; } function targetLabels(execution) { - return execution.eventName === 'pull_request' + return ['pull_request', 'pull_request_review_comment'].includes(execution.eventName) ? execution.labels.currentPullRequestLabels : execution.labels.currentIssueLabels; } function setTargetLabels(execution, labels) { - if (execution.eventName === 'pull_request') + if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) { execution.labels.currentPullRequestLabels = labels; + } else execution.labels.currentIssueLabels = labels; } function replaceLifecycleLabels(currentLabels, state, lifecycleLabels) { + if (!state) + return [...currentLabels]; const managedLabels = new Set((0, copilot_lifecycle_1.lifecycleLabelNames)(lifecycleLabels).map(label => label.toLowerCase())); const retained = currentLabels.filter(label => !managedLabels.has(label.trim().toLowerCase())); const next = (0, copilot_lifecycle_1.lifecycleStateLabel)(state, lifecycleLabels); return [...retained, next]; } +function replaceWaitingLabels(currentLabels, decision, lifecycleLabels) { + if (decision.kind === 'preserve') + return [...currentLabels]; + const managedLabels = new Set((0, copilot_lifecycle_1.waitingLabelNames)(lifecycleLabels).map(label => label.toLowerCase())); + const retained = currentLabels.filter(label => !managedLabels.has(label.trim().toLowerCase())); + if (decision.kind === 'clear') + return retained; + return [...retained, (0, copilot_lifecycle_1.waitingStateLabel)(decision.state, lifecycleLabels)]; +} +function lifecycleSynchronizationSteps(state, waitingDecision) { + const steps = []; + if (state) + steps.push(`Lifecycle state synchronized to \`${state}\`.`); + if (waitingDecision.kind === 'set') { + steps.push(`Waiting state synchronized to \`${waitingDecision.state}\`.`); + } + else if (waitingDecision.kind === 'clear') { + steps.push('Waiting state cleared.'); + } + return steps; +} function sameLabels(left, right) { return left.length === right.length && left.every((label, index) => label === right[index]); } @@ -65917,21 +66162,30 @@ function parseCopilotCommand(raw) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.DEFAULT_COPILOT_LIFECYCLE_LABELS = void 0; exports.lifecycleLabelDefinitions = lifecycleLabelDefinitions; +exports.activityLabelDefinitions = activityLabelDefinitions; +exports.waitingLabelDefinitions = waitingLabelDefinitions; +exports.managedLifecycleLabelDefinitions = managedLifecycleLabelDefinitions; exports.lifecycleLabelNames = lifecycleLabelNames; +exports.activityLabelNames = activityLabelNames; +exports.waitingLabelNames = waitingLabelNames; +exports.managedLifecycleLabelNames = managedLifecycleLabelNames; exports.lifecycleStateLabel = lifecycleStateLabel; +exports.activityLabel = activityLabel; +exports.waitingStateLabel = waitingStateLabel; exports.lifecycleStateFromLabels = lifecycleStateFromLabels; exports.DEFAULT_COPILOT_LIFECYCLE_LABELS = { - analyzing: 'copilot:state:analyzing', - planned: 'copilot:state:planned', - inProgress: 'copilot:state:in-progress', - reviewing: 'copilot:state:reviewing', - changesRequested: 'copilot:state:changes-requested', - verified: 'copilot:state:verified', - ready: 'copilot:state:ready', - blocked: 'copilot:state:blocked', + aiProcessing: 'state:ai-processing', + planned: 'state:planned', + inProgress: 'state:in-progress', + reviewing: 'state:reviewing', + changesRequested: 'state:changes-requested', + verified: 'state:verified', + ready: 'state:ready', + blocked: 'state:blocked', + awaitingMaintainer: 'state:awaiting-maintainer', + awaitingIssueAuthor: 'state:awaiting-issue-author', }; -const LIFECYCLE_METADATA = [ - ['analyzing', 'analyzing', 'FBCA04', 'Copilot is analyzing the issue or change.'], +const STABLE_LIFECYCLE_METADATA = [ ['planned', 'planned', '1D76DB', 'Copilot has produced an implementation plan.'], ['in-progress', 'inProgress', '0E8A16', 'Implementation work is in progress.'], ['reviewing', 'reviewing', '5319E7', 'A pull request is being reviewed.'], @@ -65940,23 +66194,81 @@ const LIFECYCLE_METADATA = [ ['ready', 'ready', '6F42C1', 'The change is ready for human approval or merge.'], ['blocked', 'blocked', 'B60205', 'The workflow is blocked and needs human input.'], ]; -function lifecycleLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { - return LIFECYCLE_METADATA.map(([state, key, color, description]) => ({ +const ACTIVITY_METADATA = [ + ['ai-processing', 'aiProcessing', 'FBCA04', 'A Copilot agent is analyzing or working on the issue or change.'], +]; +const WAITING_METADATA = [ + ['awaiting-maintainer', 'awaitingMaintainer', '5319E7', 'The next action requires a maintainer response or approval.'], + ['awaiting-issue-author', 'awaitingIssueAuthor', 'D93F0B', 'The next action requires more information or changes from the issue author.'], +]; +function stableDefinitions(labels) { + return STABLE_LIFECYCLE_METADATA.map(([state, key, color, description]) => ({ + category: 'lifecycle', state, name: labels[key], color, description, })); } +function activityDefinitions(labels) { + return ACTIVITY_METADATA.map(([, key, color, description]) => ({ + category: 'activity', + name: labels[key], + color, + description, + })); +} +function waitingDefinitions(labels) { + return WAITING_METADATA.map(([, key, color, description]) => ({ + category: 'waiting', + name: labels[key], + color, + description, + })); +} +function lifecycleLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return stableDefinitions(labels); +} +function activityLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return activityDefinitions(labels); +} +function waitingLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return waitingDefinitions(labels); +} +function managedLifecycleLabelDefinitions(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return [ + ...stableDefinitions(labels), + ...activityDefinitions(labels), + ...waitingDefinitions(labels), + ]; +} function lifecycleLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { return lifecycleLabelDefinitions(labels).map(definition => definition.name); } +function activityLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return activityLabelDefinitions(labels).map(definition => definition.name); +} +function waitingLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return waitingLabelDefinitions(labels).map(definition => definition.name); +} +function managedLifecycleLabelNames(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return managedLifecycleLabelDefinitions(labels).map(definition => definition.name); +} function lifecycleStateLabel(state, labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { const definition = lifecycleLabelDefinitions(labels).find(candidate => candidate.state === state); if (!definition) throw new Error(`Unknown Copilot lifecycle state: ${state}`); return definition.name; } +function activityLabel(labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + return labels.aiProcessing; +} +function waitingStateLabel(state, labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { + const metadata = WAITING_METADATA.find(([metadataState]) => metadataState === state); + if (!metadata) + throw new Error(`Unknown Copilot waiting state: ${state}`); + return labels[metadata[1]]; +} function lifecycleStateFromLabels(currentLabels, labels = exports.DEFAULT_COPILOT_LIFECYCLE_LABELS) { const normalized = new Set(currentLabels.map(label => label.trim().toLowerCase())); return lifecycleLabelDefinitions(labels).find(definition => normalized.has(definition.name.trim().toLowerCase()))?.state; @@ -66117,6 +66429,22 @@ function createActorAuthorizationRepository() { } +/***/ }), + +/***/ 94253: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSynchronizeAgentActivityUseCase = createSynchronizeAgentActivityUseCase; +const synchronize_agent_activity_use_case_1 = __nccwpck_require__(44880); +const issue_labels_composition_root_1 = __nccwpck_require__(34780); +function createSynchronizeAgentActivityUseCase() { + return new synchronize_agent_activity_use_case_1.SynchronizeAgentActivityUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)()); +} + + /***/ }), /***/ 85079: @@ -68670,15 +68998,17 @@ exports.INPUT_KEYS = { SIZE_M_LABEL: 'size-m-label', SIZE_S_LABEL: 'size-s-label', SIZE_XS_LABEL: 'size-xs-label', - // Copilot lifecycle labels - COPILOT_STATE_ANALYZING_LABEL: 'copilot-state-analyzing-label', - COPILOT_STATE_PLANNED_LABEL: 'copilot-state-planned-label', - COPILOT_STATE_IN_PROGRESS_LABEL: 'copilot-state-in-progress-label', - COPILOT_STATE_REVIEWING_LABEL: 'copilot-state-reviewing-label', - COPILOT_STATE_CHANGES_REQUESTED_LABEL: 'copilot-state-changes-requested-label', - COPILOT_STATE_VERIFIED_LABEL: 'copilot-state-verified-label', - COPILOT_STATE_READY_LABEL: 'copilot-state-ready-label', - COPILOT_STATE_BLOCKED_LABEL: 'copilot-state-blocked-label', + // Lifecycle label inputs + STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', + STATE_PLANNED_LABEL: 'state-planned-label', + STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', + STATE_REVIEWING_LABEL: 'state-reviewing-label', + STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', + STATE_VERIFIED_LABEL: 'state-verified-label', + STATE_READY_LABEL: 'state-ready-label', + STATE_BLOCKED_LABEL: 'state-blocked-label', + STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', + STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', // Issue Types ISSUE_TYPE_BUG: 'issue-type-bug', ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', diff --git a/build/github_action/src/actions/common_action.d.ts b/build/github_action/src/actions/common_action.d.ts index 5bc3b1db..5b51e419 100644 --- a/build/github_action/src/actions/common_action.d.ts +++ b/build/github_action/src/actions/common_action.d.ts @@ -3,4 +3,5 @@ import { Result } from '../data/model/result'; import { ProjectBoardCommandPort } from '../application/ports/project_board_command_ports'; import type { LatestTagQueryPort } from '../application/ports/branch_tag_ports'; import type { SynchronizeLifecycleStateUseCase } from '../application/usecases/actions/synchronize_lifecycle_state_use_case'; -export declare function mainRun(execution: Execution, projectBoardCommandPort: ProjectBoardCommandPort, latestTagQueryPort: LatestTagQueryPort, lifecycleStateUseCase?: SynchronizeLifecycleStateUseCase): Promise; +import type { SynchronizeAgentActivityUseCase } from '../application/usecases/actions/synchronize_agent_activity_use_case'; +export declare function mainRun(execution: Execution, projectBoardCommandPort: ProjectBoardCommandPort, latestTagQueryPort: LatestTagQueryPort, lifecycleStateUseCase?: SynchronizeLifecycleStateUseCase, agentActivityUseCase?: SynchronizeAgentActivityUseCase): Promise; diff --git a/build/github_action/src/actions/local_action_configuration.d.ts b/build/github_action/src/actions/local_action_configuration.d.ts index aca66b8f..748e9222 100644 --- a/build/github_action/src/actions/local_action_configuration.d.ts +++ b/build/github_action/src/actions/local_action_configuration.d.ts @@ -94,7 +94,7 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn sizeSLabel: string; sizeXsLabel: string; lifecycle: { - analyzing: string; + aiProcessing: string; planned: string; inProgress: string; reviewing: string; @@ -102,6 +102,8 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn verified: string; ready: string; blocked: string; + awaitingMaintainer: string; + awaitingIssueAuthor: string; }; projectIdsInput: string; projectIds: string[]; diff --git a/build/github_action/src/actions/local_action_configuration_sections.d.ts b/build/github_action/src/actions/local_action_configuration_sections.d.ts index fc09d584..740ed554 100644 --- a/build/github_action/src/actions/local_action_configuration_sections.d.ts +++ b/build/github_action/src/actions/local_action_configuration_sections.d.ts @@ -65,7 +65,7 @@ export declare function readLocalLabelsAndIssueTypes(additionalParams: ActionInp sizeSLabel: string; sizeXsLabel: string; lifecycle: { - analyzing: string; + aiProcessing: string; planned: string; inProgress: string; reviewing: string; @@ -73,6 +73,8 @@ export declare function readLocalLabelsAndIssueTypes(additionalParams: ActionInp verified: string; ready: string; blocked: string; + awaitingMaintainer: string; + awaitingIssueAuthor: string; }; }; issueTypes: { diff --git a/build/github_action/src/application/policies/agent_activity_label_policy.d.ts b/build/github_action/src/application/policies/agent_activity_label_policy.d.ts new file mode 100644 index 00000000..9bfa5fa6 --- /dev/null +++ b/build/github_action/src/application/policies/agent_activity_label_policy.d.ts @@ -0,0 +1,2 @@ +/** Adds or removes one activity label without touching unrelated labels. */ +export declare function replaceAgentActivityLabel(currentLabels: readonly string[], activityLabel: string, active: boolean): string[]; diff --git a/build/github_action/src/application/policies/agent_activity_policy.d.ts b/build/github_action/src/application/policies/agent_activity_policy.d.ts new file mode 100644 index 00000000..b26fe59f --- /dev/null +++ b/build/github_action/src/application/policies/agent_activity_policy.d.ts @@ -0,0 +1,4 @@ +import type { Execution } from '../../data/model/execution'; +export type AgentActivityRoute = 'single-action' | 'issue-comment' | 'issue' | 'pull-request-review-comment' | 'pull-request' | 'push'; +/** Decides whether a route can invoke an agent for its current event. */ +export declare function shouldTrackAgentActivity(execution: Execution, route: AgentActivityRoute): boolean; diff --git a/build/github_action/src/application/policies/lifecycle_waiting_state_policy.d.ts b/build/github_action/src/application/policies/lifecycle_waiting_state_policy.d.ts new file mode 100644 index 00000000..a5d3091f --- /dev/null +++ b/build/github_action/src/application/policies/lifecycle_waiting_state_policy.d.ts @@ -0,0 +1,18 @@ +import type { CopilotLifecycleState, CopilotWaitingState } from '../../domain/copilot_lifecycle'; +export type LifecycleWaitingStateDecision = { + kind: 'set'; + state: CopilotWaitingState; +} | { + kind: 'clear'; +} | { + kind: 'preserve'; +}; +export interface LifecycleWaitingStateInput { + readonly eventName: string; + readonly lifecycleState: CopilotLifecycleState | undefined; +} +/** + * Resolves who should provide the next human input. Waiting labels are + * orthogonal to the stable lifecycle phase and at most one is retained. + */ +export declare function resolveLifecycleWaitingState(input: LifecycleWaitingStateInput): LifecycleWaitingStateDecision; diff --git a/build/github_action/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts b/build/github_action/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts new file mode 100644 index 00000000..5b4fb8ca --- /dev/null +++ b/build/github_action/src/application/usecases/actions/synchronize_agent_activity_use_case.d.ts @@ -0,0 +1,16 @@ +import type { Execution } from '../../../data/model/execution'; +import type { IssueLabelsPort } from '../../ports/issue_management_ports'; +/** + * Maintains the temporary agent-activity label around a complete route. + * Cleanup is deliberately best-effort so a label outage never hides the + * actual route result; the in-memory execution remains synchronized after a + * successful mutation so later lifecycle writes preserve the activity label. + */ +export declare class SynchronizeAgentActivityUseCase { + private readonly issueLabelsPort; + readonly taskId = "SynchronizeAgentActivityUseCase"; + constructor(issueLabelsPort: IssueLabelsPort); + start(execution: Execution): Promise; + finish(execution: Execution): Promise; + private synchronize; +} diff --git a/build/github_action/src/domain/copilot_lifecycle.d.ts b/build/github_action/src/domain/copilot_lifecycle.d.ts index ab840934..a9e5f070 100644 --- a/build/github_action/src/domain/copilot_lifecycle.d.ts +++ b/build/github_action/src/domain/copilot_lifecycle.d.ts @@ -1,11 +1,14 @@ /** - * The Copilot lifecycle is deliberately independent from GitHub's API model. - * Labels are the persistence representation; this policy is the state machine - * used by application workflows and can therefore be tested without I/O. + * Copilot labels are split into independent dimensions. A durable lifecycle + * phase can coexist with temporary agent activity and a human waiting state. + * This policy is independent from GitHub's API model and remains unit-testable + * without I/O. */ -export type CopilotLifecycleState = 'analyzing' | 'planned' | 'in-progress' | 'reviewing' | 'changes-requested' | 'verified' | 'ready' | 'blocked'; +export type CopilotLifecycleState = 'planned' | 'in-progress' | 'reviewing' | 'changes-requested' | 'verified' | 'ready' | 'blocked'; +export type CopilotAgentActivity = 'ai-processing'; +export type CopilotWaitingState = 'awaiting-maintainer' | 'awaiting-issue-author'; export interface CopilotLifecycleLabels { - analyzing: string; + aiProcessing: string; planned: string; inProgress: string; reviewing: string; @@ -13,15 +16,27 @@ export interface CopilotLifecycleLabels { verified: string; ready: string; blocked: string; + awaitingMaintainer: string; + awaitingIssueAuthor: string; } export declare const DEFAULT_COPILOT_LIFECYCLE_LABELS: Readonly; +export type LifecycleLabelCategory = 'lifecycle' | 'activity' | 'waiting'; export interface LifecycleLabelDefinition { - readonly state: CopilotLifecycleState; + readonly category: LifecycleLabelCategory; + readonly state?: CopilotLifecycleState; readonly name: string; readonly color: string; readonly description: string; } export declare function lifecycleLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; +export declare function activityLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; +export declare function waitingLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; +export declare function managedLifecycleLabelDefinitions(labels?: CopilotLifecycleLabels): LifecycleLabelDefinition[]; export declare function lifecycleLabelNames(labels?: CopilotLifecycleLabels): string[]; +export declare function activityLabelNames(labels?: CopilotLifecycleLabels): string[]; +export declare function waitingLabelNames(labels?: CopilotLifecycleLabels): string[]; +export declare function managedLifecycleLabelNames(labels?: CopilotLifecycleLabels): string[]; export declare function lifecycleStateLabel(state: CopilotLifecycleState, labels?: CopilotLifecycleLabels): string; +export declare function activityLabel(labels?: CopilotLifecycleLabels): string; +export declare function waitingStateLabel(state: CopilotWaitingState, labels?: CopilotLifecycleLabels): string; export declare function lifecycleStateFromLabels(currentLabels: readonly string[], labels?: CopilotLifecycleLabels): CopilotLifecycleState | undefined; diff --git a/build/github_action/src/infrastructure/composition/agent_activity_composition_root.d.ts b/build/github_action/src/infrastructure/composition/agent_activity_composition_root.d.ts new file mode 100644 index 00000000..4ed82efb --- /dev/null +++ b/build/github_action/src/infrastructure/composition/agent_activity_composition_root.d.ts @@ -0,0 +1,2 @@ +import { SynchronizeAgentActivityUseCase } from '../../application/usecases/actions/synchronize_agent_activity_use_case'; +export declare function createSynchronizeAgentActivityUseCase(): SynchronizeAgentActivityUseCase; diff --git a/build/github_action/src/utils/constants.d.ts b/build/github_action/src/utils/constants.d.ts index 8512680b..43f98677 100644 --- a/build/github_action/src/utils/constants.d.ts +++ b/build/github_action/src/utils/constants.d.ts @@ -154,14 +154,16 @@ export declare const INPUT_KEYS: { readonly SIZE_M_LABEL: "size-m-label"; readonly SIZE_S_LABEL: "size-s-label"; readonly SIZE_XS_LABEL: "size-xs-label"; - readonly COPILOT_STATE_ANALYZING_LABEL: "copilot-state-analyzing-label"; - readonly COPILOT_STATE_PLANNED_LABEL: "copilot-state-planned-label"; - readonly COPILOT_STATE_IN_PROGRESS_LABEL: "copilot-state-in-progress-label"; - readonly COPILOT_STATE_REVIEWING_LABEL: "copilot-state-reviewing-label"; - readonly COPILOT_STATE_CHANGES_REQUESTED_LABEL: "copilot-state-changes-requested-label"; - readonly COPILOT_STATE_VERIFIED_LABEL: "copilot-state-verified-label"; - readonly COPILOT_STATE_READY_LABEL: "copilot-state-ready-label"; - readonly COPILOT_STATE_BLOCKED_LABEL: "copilot-state-blocked-label"; + readonly STATE_AI_PROCESSING_LABEL: "state-ai-processing-label"; + readonly STATE_PLANNED_LABEL: "state-planned-label"; + readonly STATE_IN_PROGRESS_LABEL: "state-in-progress-label"; + readonly STATE_REVIEWING_LABEL: "state-reviewing-label"; + readonly STATE_CHANGES_REQUESTED_LABEL: "state-changes-requested-label"; + readonly STATE_VERIFIED_LABEL: "state-verified-label"; + readonly STATE_READY_LABEL: "state-ready-label"; + readonly STATE_BLOCKED_LABEL: "state-blocked-label"; + readonly STATE_AWAITING_MAINTAINER_LABEL: "state-awaiting-maintainer-label"; + readonly STATE_AWAITING_ISSUE_AUTHOR_LABEL: "state-awaiting-issue-author-label"; readonly ISSUE_TYPE_BUG: "issue-type-bug"; readonly ISSUE_TYPE_BUG_DESCRIPTION: "issue-type-bug-description"; readonly ISSUE_TYPE_BUG_COLOR: "issue-type-bug-color"; diff --git a/docs/features.mdx b/docs/features.mdx index 47c86440..a0500aa1 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -40,7 +40,7 @@ When the workflow runs on `issues` (opened, edited, labeled, unlabeled, etc.): | **Size labels** | Assigns size labels (XS–XXL) and checks size thresholds (lines, files, commits) for prioritization. | | **Comments & images** | Posts comments with optional images (per branch type: feature, bugfix, docs, chore, hotfix, release). | | **Smart workflow guidance** | Comments can include Git-Flow reminders and next steps. | -| **Lifecycle state** | Maintains one setup-provisioned `copilot:state:*` label so the issue visibly moves from analysis to planning and implementation. | +| **Lifecycle labels** | Maintains an exclusive durable `state:*` phase, an optional `state:ai-processing` activity marker, and an optional human-waiting label. Activity can coexist with the durable phase and is removed when the agent run finishes. | ### 2. Pull request events (`on: pull_request`) diff --git a/docs/how-to-use.mdx b/docs/how-to-use.mdx index 78cd821d..e200153c 100644 --- a/docs/how-to-use.mdx +++ b/docs/how-to-use.mdx @@ -119,7 +119,7 @@ The complete command reference, including every supported option, is in [Workflo - Create `.github/`, `.github/workflows/`, and `.github/ISSUE_TEMPLATE/` if they do not exist. - Copy all files from the Copilot `setup/` folder into your repo (workflows, issue templates, pull request template). Existing files are **not** overwritten. - Verify GitHub access using `PERSONAL_ACCESS_TOKEN` from `.env`, the environment, or the `--token` option. - - Create all required **labels** in the repository (type, action, priority, size, progress 0%–100%). + - Create all required **labels** in the repository (type, action, priority, size, progress 0%–100%, and the lifecycle/activity/waiting labels). - Create all required **issue types** in the organization (Task, Bug, Feature, Release, Hotfix, etc.), if your plan supports it. After this step you have a working baseline: labels, templates, and workflow files are in place. @@ -209,6 +209,25 @@ The setup creates the following labels. Names come from the action input default The setup creates **21 progress labels**: `0%`, `5%`, `10%`, … `100%`. Colors go from red (0%) to yellow (50%) to green (100%). These are used by the Check Progress action and commit steps to update issue progress. +### Lifecycle, activity, and waiting labels + +The setup creates three related label dimensions. The durable lifecycle phase is exclusive; the temporary activity label and the waiting label are independent, so a target can have (for example) `state:in-progress` and `state:ai-processing` at the same time. + +| Input key | Default label name | Purpose | +|-----------|--------------------|--------| +| `state-planned-label` | `state:planned` | An implementation plan is available | +| `state-in-progress-label` | `state:in-progress` | Implementation has started | +| `state-reviewing-label` | `state:reviewing` | A pull request is under review | +| `state-changes-requested-label` | `state:changes-requested` | The review has active findings | +| `state-verified-label` | `state:verified` | The pull request was merged successfully | +| `state-ready-label` | `state:ready` | The latest review is ready for human action | +| `state-blocked-label` | `state:blocked` | The workflow needs human intervention | +| `state-ai-processing-label` | `state:ai-processing` | Temporary marker while a Copilot agent is working; removed at run end | +| `state-awaiting-maintainer-label` | `state:awaiting-maintainer` | Waiting for maintainer response, approval, or merge | +| `state-awaiting-issue-author-label` | `state:awaiting-issue-author` | Waiting for information or changes from the issue author | + +Existing `copilot:state:*` labels are not migrated automatically and can be removed manually. + --- ## Issue types created by `copilot setup` (defaults) diff --git a/docs/issues/index.mdx b/docs/issues/index.mdx index 87da4256..90a10767 100644 --- a/docs/issues/index.mdx +++ b/docs/issues/index.mdx @@ -44,20 +44,31 @@ Copilot automates **issue tracking** so that labels, branch creation, project li ## Lifecycle state labels -During setup, Copilot creates the following state labels once. They are managed as a single mutually-exclusive state label; all business labels such as `feature`, `bugfix`, `priority`, and `branched` are preserved. +During setup, Copilot creates the lifecycle labels once. The durable lifecycle phase is mutually exclusive, while the activity and waiting dimensions are independent. This means labels such as `state:in-progress` and `state:ai-processing` can exist together. Normal business labels such as `feature`, `bugfix`, `priority`, and `branched` are preserved. -| State | Meaning | +### Durable lifecycle phase + +| Label | Meaning | +| --- | --- | +| `state:planned` | A recommendation or implementation plan is available. | +| `state:in-progress` | Branch-based implementation has started. | +| `state:reviewing` | A pull request is being reviewed. | +| `state:changes-requested` | The review has active findings. | +| `state:ready` | The latest review has no active findings. | +| `state:verified` | The pull request was merged successfully. | +| `state:blocked` | A workflow failed or needs human input. | + +### Temporary activity and human waiting + +| Label | Meaning | | --- | --- | -| `copilot:state:analyzing` | The issue or change is being analyzed. | -| `copilot:state:planned` | A recommendation or implementation plan is available. | -| `copilot:state:in-progress` | Branch-based implementation has started. | -| `copilot:state:reviewing` | A pull request is being reviewed. | -| `copilot:state:changes-requested` | The review has active findings. | -| `copilot:state:ready` | The latest review has no active findings. | -| `copilot:state:verified` | The pull request was merged successfully. | -| `copilot:state:blocked` | A workflow failed or needs human input. | - -The names are configurable through the action inputs, but the labels are provisioned by `copilot setup` in the destination repository. They are not created dynamically during normal issue or PR runs. +| `state:ai-processing` | A Copilot agent is currently analyzing or working. It is removed when that run finishes. | +| `state:awaiting-maintainer` | The next action requires a maintainer response, approval, or merge. | +| `state:awaiting-issue-author` | The next action requires more information or changes from the issue author. | + +At most one durable phase and one waiting label are synchronized at a time. The temporary `state:ai-processing` label can coexist with either dimension and is best-effort: a label API failure does not hide the actual agent result. The label names are configurable through the action inputs, and all ten defaults are provisioned by `copilot setup` in the destination repository. + +Existing legacy labels such as `copilot:state:ready` are intentionally not migrated or removed automatically. They can be deleted manually after adopting the `state:*` labels. ## Daily agent workflow diff --git a/docs/pull-requests/capabilities.mdx b/docs/pull-requests/capabilities.mdx index 2a40721e..eff2012b 100644 --- a/docs/pull-requests/capabilities.mdx +++ b/docs/pull-requests/capabilities.mdx @@ -72,7 +72,7 @@ Use this for team branding or to show different visuals per type of PR. See [Con ## Review state and evidence -The PR workflow maintains one lifecycle state label (`reviewing`, `changes-requested`, or `ready`) while preserving the normal business labels. Each run also writes a bounded Job Summary. When the PAT can create Checks, Copilot publishes a stable `Copilot / Review` Check Run for the PR head; the check fails when active or reopened findings remain. A missing Checks capability does not make the functional workflow fail. +The PR workflow maintains one durable lifecycle label (`state:reviewing`, `state:changes-requested`, or `state:ready`) while preserving the normal business labels. Active findings also set `state:awaiting-issue-author`; a clean review sets `state:awaiting-maintainer`. While the reviewer is running, `state:ai-processing` may coexist with these labels and is removed at the end of the run. Each run also writes a bounded Job Summary. When the PAT can create Checks, Copilot publishes a stable `Copilot / Review` Check Run for the PR head; the check fails when active or reopened findings remain. A missing Checks capability does not make the functional workflow fail. ## Next steps diff --git a/docs/pull-requests/workflow-setup.mdx b/docs/pull-requests/workflow-setup.mdx index e4b11178..ed1dbb36 100644 --- a/docs/pull-requests/workflow-setup.mdx +++ b/docs/pull-requests/workflow-setup.mdx @@ -74,7 +74,7 @@ jobs: 7. **Comments and images:** The action can post a comment on the PR with optional images per branch type (feature, bugfix, etc.). See [Capabilities](/pull-requests/capabilities). -8. **Bugbot review:** On `opened`, `reopened`, `edited`, and `synchronize`, the reviewer role analyzes the PR head and publishes stable finding comments. Active findings move the PR to `copilot:state:changes-requested`; a clean review moves it to `copilot:state:ready`. Configure `reviewer-provider`, `reviewer-model-provider`, `reviewer-model`, `reviewer-effort`, and `reviewer-command` only when the reviewer should differ from the base agent. +8. **Bugbot review:** On `opened`, `reopened`, `edited`, and `synchronize`, the reviewer role analyzes the PR head and publishes stable finding comments. Active findings move the PR to `state:changes-requested` and `state:awaiting-issue-author`; a clean review moves it to `state:ready` and `state:awaiting-maintainer`. During the run, `state:ai-processing` may coexist with those labels and is removed when the agent finishes. Configure `reviewer-provider`, `reviewer-model-provider`, `reviewer-model`, `reviewer-effort`, and `reviewer-command` only when the reviewer should differ from the base agent. ## Next steps diff --git a/src/actions/__tests__/common_action.test.ts b/src/actions/__tests__/common_action.test.ts index 07b87d0c..c52320c4 100644 --- a/src/actions/__tests__/common_action.test.ts +++ b/src/actions/__tests__/common_action.test.ts @@ -17,6 +17,7 @@ import { createMainRunRouteCompositionRoot } from '../../infrastructure/composit import type { ProjectBoardCommandPort } from '../../application/ports/project_board_command_ports'; import type { LatestTagQueryPort } from '../../application/ports/branch_tag_ports'; import type { Execution } from '../../data/model/execution'; +import type { SynchronizeAgentActivityUseCase } from '../../application/usecases/actions/synchronize_agent_activity_use_case'; import { Result } from '../../data/model/result'; import { logInfo } from '../../utils/logger'; @@ -39,6 +40,8 @@ const mockPullRequestInvoke = jest.fn(); const mockCommitInvoke = jest.fn(); const mockSetupExecutionInvoke = jest.fn(); const mockWaitForPreviousWorkflowRunsInvoke = jest.fn(); +const mockAgentActivityStart = jest.fn(); +const mockAgentActivityFinish = jest.fn(); jest.mock('../../infrastructure/composition/main_run_route_composition_root', () => ({ createMainRunRouteCompositionRoot: jest.fn().mockImplementation(() => ({ @@ -132,6 +135,17 @@ const runMain = (execution: Execution) => productionMainRun( latestTagQueryPort, ); +const runMainWithActivity = (execution: Execution) => productionMainRun( + execution, + projectBoardCommandPort, + latestTagQueryPort, + undefined, + { + start: mockAgentActivityStart, + finish: mockAgentActivityFinish, + } as unknown as SynchronizeAgentActivityUseCase, +); + const originalRunId = process.env.GITHUB_RUN_ID; const originalWorkflow = process.env.GITHUB_WORKFLOW; const originalWorkflowRef = process.env.GITHUB_WORKFLOW_REF; @@ -156,6 +170,8 @@ describe('mainRun', () => { mockPullRequestInvoke.mockResolvedValue([]); mockCommitInvoke.mockResolvedValue([]); mockSetupExecutionInvoke.mockResolvedValue(undefined); + mockAgentActivityStart.mockResolvedValue(undefined); + mockAgentActivityFinish.mockResolvedValue(undefined); }); afterEach(() => { @@ -391,6 +407,26 @@ describe('mainRun', () => { expect(results).toEqual(expected); }); + it('tracks agent activity around an agent-backed route', async () => { + const order: string[] = []; + mockAgentActivityStart.mockImplementation(async () => { order.push('activity-start'); }); + mockCommitInvoke.mockImplementation(async () => { order.push('route'); return []; }); + mockAgentActivityFinish.mockImplementation(async () => { order.push('activity-finish'); }); + const execution = mockExecution({ + eventName: 'push', + isPush: true, + commit: { commits: [{ id: 'commit-1' }] }, + ai: { + getAgentConfiguration: jest.fn(() => ({ model: 'model', command: 'agent' })), + getAiPullRequestDescription: jest.fn(() => false), + }, + }); + + await runMainWithActivity(execution); + + expect(order).toEqual(['activity-start', 'route', 'activity-finish']); + }); + it('calls core.setFailed when action not handled', async () => { const execution = mockExecution({ isIssue: false, diff --git a/src/actions/common_action.ts b/src/actions/common_action.ts index e79d78ea..8504559f 100644 --- a/src/actions/common_action.ts +++ b/src/actions/common_action.ts @@ -10,6 +10,8 @@ import { requireRepositoryCoordinates } from './repository_context'; import { configureApplicationLogger } from '../application/ports/logging_ports'; import { createLoggerAdapter } from '../infrastructure/logging/logger_adapter'; import type { SynchronizeLifecycleStateUseCase } from '../application/usecases/actions/synchronize_lifecycle_state_use_case'; +import type { SynchronizeAgentActivityUseCase } from '../application/usecases/actions/synchronize_agent_activity_use_case'; +import { shouldTrackAgentActivity, type AgentActivityRoute } from '../application/policies/agent_activity_policy'; import { logWelcomeMessage, runMainRoute, @@ -23,6 +25,7 @@ export async function mainRun( projectBoardCommandPort: ProjectBoardCommandPort, latestTagQueryPort: LatestTagQueryPort, lifecycleStateUseCase?: SynchronizeLifecycleStateUseCase, + agentActivityUseCase?: SynchronizeAgentActivityUseCase, ): Promise { configureApplicationLogger(createLoggerAdapter()); const repository = requireRepositoryCoordinates({ @@ -46,11 +49,11 @@ export async function mainRun( const routeHandlers = createMainRunRouteCompositionRoot(projectBoardCommandPort); if (execution.runnedByToken) { - return runTokenExecution(execution, routeHandlers); + return runTrackedRoute(execution, 'single-action', () => runTokenExecution(execution, routeHandlers), undefined, agentActivityUseCase); } if (execution.issueNumber === -1) { - return runNoIssueExecution(execution, routeHandlers); + return runTrackedRoute(execution, 'single-action', () => runNoIssueExecution(execution, routeHandlers), undefined, agentActivityUseCase); } logWelcomeMessage(execution); @@ -62,7 +65,31 @@ export async function mainRun( isPullRequestReviewComment: execution.pullRequest.isPullRequestReviewComment, isPush: execution.isPush, }); - const results = await runMainRoute(execution, route, routeHandlers); - if (!lifecycleStateUseCase) return results; - return [...results, ...(await lifecycleStateUseCase.invoke({ execution, results }))]; + if (route === 'unhandled') return runMainRoute(execution, route, routeHandlers); + return runTrackedRoute( + execution, + route, + () => runMainRoute(execution, route, routeHandlers), + lifecycleStateUseCase, + agentActivityUseCase, + ); +} + +async function runTrackedRoute( + execution: Execution, + route: AgentActivityRoute, + run: () => Promise, + lifecycleStateUseCase: SynchronizeLifecycleStateUseCase | undefined, + agentActivityUseCase: SynchronizeAgentActivityUseCase | undefined, +): Promise { + const trackActivity = agentActivityUseCase !== undefined && shouldTrackAgentActivity(execution, route); + if (trackActivity) await agentActivityUseCase.start(execution); + + try { + const results = await run(); + if (!lifecycleStateUseCase) return results; + return [...results, ...(await lifecycleStateUseCase.invoke({ execution, results }))]; + } finally { + if (trackActivity) await agentActivityUseCase.finish(execution); + } } diff --git a/src/actions/github_action.ts b/src/actions/github_action.ts index 4d9c99eb..3e725aef 100644 --- a/src/actions/github_action.ts +++ b/src/actions/github_action.ts @@ -18,6 +18,7 @@ import { createGithubExecutionAdmissionUseCase } from '../infrastructure/composi import { createSynchronizeLifecycleStateUseCase } from '../infrastructure/composition/lifecycle_state_composition_root'; import { createCopilotEvidenceCompositionRoot } from '../infrastructure/composition/copilot_evidence_composition_root'; import { createGithubActionSummaryCompositionRoot } from '../infrastructure/composition/github_action_summary_composition_root'; +import { createSynchronizeAgentActivityUseCase } from '../infrastructure/composition/agent_activity_composition_root'; export async function runGitHubAction(): Promise { if (isEnabledInput(getGithubActionInput(INPUT_KEYS.QUEUE_GATE_ONLY))) { @@ -71,6 +72,7 @@ export async function runGitHubAction(): Promise { projectBoard.command, new GitCliRepository(), createSynchronizeLifecycleStateUseCase(), + createSynchronizeAgentActivityUseCase(), ); const issueContentPort = createIssueContentCompositionRoot(); await finishGithubAction( diff --git a/src/actions/github_action_label_inputs.ts b/src/actions/github_action_label_inputs.ts index fcfc4222..12edf813 100644 --- a/src/actions/github_action_label_inputs.ts +++ b/src/actions/github_action_label_inputs.ts @@ -23,14 +23,16 @@ export function readGithubActionLabelInputs(getInput: (key: string) => string): s: getInput(INPUT_KEYS.SIZE_S_LABEL), xs: getInput(INPUT_KEYS.SIZE_XS_LABEL), }, lifecycle: { - analyzing: getInput(INPUT_KEYS.COPILOT_STATE_ANALYZING_LABEL), - planned: getInput(INPUT_KEYS.COPILOT_STATE_PLANNED_LABEL), - inProgress: getInput(INPUT_KEYS.COPILOT_STATE_IN_PROGRESS_LABEL), - reviewing: getInput(INPUT_KEYS.COPILOT_STATE_REVIEWING_LABEL), - changesRequested: getInput(INPUT_KEYS.COPILOT_STATE_CHANGES_REQUESTED_LABEL), - verified: getInput(INPUT_KEYS.COPILOT_STATE_VERIFIED_LABEL), - ready: getInput(INPUT_KEYS.COPILOT_STATE_READY_LABEL), - blocked: getInput(INPUT_KEYS.COPILOT_STATE_BLOCKED_LABEL), + aiProcessing: getInput(INPUT_KEYS.STATE_AI_PROCESSING_LABEL), + planned: getInput(INPUT_KEYS.STATE_PLANNED_LABEL), + inProgress: getInput(INPUT_KEYS.STATE_IN_PROGRESS_LABEL), + reviewing: getInput(INPUT_KEYS.STATE_REVIEWING_LABEL), + changesRequested: getInput(INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), + verified: getInput(INPUT_KEYS.STATE_VERIFIED_LABEL), + ready: getInput(INPUT_KEYS.STATE_READY_LABEL), + blocked: getInput(INPUT_KEYS.STATE_BLOCKED_LABEL), + awaitingMaintainer: getInput(INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), + awaitingIssueAuthor: getInput(INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), }, }; } diff --git a/src/actions/local_action.ts b/src/actions/local_action.ts index fde39d23..9598baec 100644 --- a/src/actions/local_action.ts +++ b/src/actions/local_action.ts @@ -14,6 +14,7 @@ import { renderLocalActionResults } from './local_action_output'; import { buildLocalActionConfiguration } from './local_action_configuration'; import { buildLocalActionExecution } from './local_action_execution'; import { requireRepositoryCoordinates } from './repository_context'; +import { createSynchronizeAgentActivityUseCase } from '../infrastructure/composition/agent_activity_composition_root'; export async function runLocalAction( additionalParams: Record @@ -25,7 +26,13 @@ export async function runLocalAction( const configuration = await buildLocalActionConfiguration(normalizedParams, composition.projectBoard.query); const execution = buildLocalActionExecution(configuration, normalizedParams); - const results = await mainRun(execution, composition.projectBoard.command, composition.latestTagQuery); + const results = await mainRun( + execution, + composition.projectBoard.command, + composition.latestTagQuery, + undefined, + createSynchronizeAgentActivityUseCase(), + ); renderLocalActionResults(results); } diff --git a/src/actions/local_action_configuration_sections.ts b/src/actions/local_action_configuration_sections.ts index cd12eada..122fc047 100644 --- a/src/actions/local_action_configuration_sections.ts +++ b/src/actions/local_action_configuration_sections.ts @@ -145,14 +145,16 @@ export function readLocalLabelsAndIssueTypes( sizeSLabel: label(INPUT_KEYS.SIZE_S_LABEL), sizeXsLabel: label(INPUT_KEYS.SIZE_XS_LABEL), lifecycle: { - analyzing: label(INPUT_KEYS.COPILOT_STATE_ANALYZING_LABEL), - planned: label(INPUT_KEYS.COPILOT_STATE_PLANNED_LABEL), - inProgress: label(INPUT_KEYS.COPILOT_STATE_IN_PROGRESS_LABEL), - reviewing: label(INPUT_KEYS.COPILOT_STATE_REVIEWING_LABEL), - changesRequested: label(INPUT_KEYS.COPILOT_STATE_CHANGES_REQUESTED_LABEL), - verified: label(INPUT_KEYS.COPILOT_STATE_VERIFIED_LABEL), - ready: label(INPUT_KEYS.COPILOT_STATE_READY_LABEL), - blocked: label(INPUT_KEYS.COPILOT_STATE_BLOCKED_LABEL), + aiProcessing: label(INPUT_KEYS.STATE_AI_PROCESSING_LABEL), + planned: label(INPUT_KEYS.STATE_PLANNED_LABEL), + inProgress: label(INPUT_KEYS.STATE_IN_PROGRESS_LABEL), + reviewing: label(INPUT_KEYS.STATE_REVIEWING_LABEL), + changesRequested: label(INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), + verified: label(INPUT_KEYS.STATE_VERIFIED_LABEL), + ready: label(INPUT_KEYS.STATE_READY_LABEL), + blocked: label(INPUT_KEYS.STATE_BLOCKED_LABEL), + awaitingMaintainer: label(INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), + awaitingIssueAuthor: label(INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), }, }, issueTypes: { diff --git a/src/application/policies/__tests__/agent_activity_label_policy.test.ts b/src/application/policies/__tests__/agent_activity_label_policy.test.ts new file mode 100644 index 00000000..ff7b7e18 --- /dev/null +++ b/src/application/policies/__tests__/agent_activity_label_policy.test.ts @@ -0,0 +1,27 @@ +import { replaceAgentActivityLabel } from '../agent_activity_label_policy'; + +describe('agent activity label policy', () => { + it('adds the activity label without touching stable or waiting labels', () => { + expect(replaceAgentActivityLabel( + ['feature', 'state:in-progress', 'state:awaiting-maintainer'], + 'state:ai-processing', + true, + )).toEqual(['feature', 'state:in-progress', 'state:awaiting-maintainer', 'state:ai-processing']); + }); + + it('removes the activity label case-insensitively', () => { + expect(replaceAgentActivityLabel( + ['feature', 'STATE:AI-PROCESSING', 'state:ready'], + 'state:ai-processing', + false, + )).toEqual(['feature', 'state:ready']); + }); + + it('does not duplicate the activity label', () => { + expect(replaceAgentActivityLabel( + ['state:ai-processing', 'STATE:AI-PROCESSING'], + 'state:ai-processing', + true, + )).toEqual(['state:ai-processing']); + }); +}); diff --git a/src/application/policies/__tests__/agent_activity_policy.test.ts b/src/application/policies/__tests__/agent_activity_policy.test.ts new file mode 100644 index 00000000..f4b76680 --- /dev/null +++ b/src/application/policies/__tests__/agent_activity_policy.test.ts @@ -0,0 +1,105 @@ +import { shouldTrackAgentActivity } from '../agent_activity_policy'; + +function execution(overrides: Record = {}): any { + return { + eventName: 'issues', + issueNumber: 7, + issue: { number: 7, opened: true, descriptionEdited: false, commentBody: '' }, + pullRequest: { number: 0, action: '', commentBody: '' }, + commit: { commits: [] }, + singleAction: { + isThinkAction: false, + isRecommendStepsAction: false, + isCheckProgressAction: false, + isDetectPotentialProblemsAction: false, + }, + ai: { + getAgentConfiguration: jest.fn(() => ({ model: 'model', command: 'agent' })), + getAiPullRequestDescription: jest.fn(() => false), + }, + ...overrides, + }; +} + +describe('agent activity policy', () => { + it('tracks issue analysis only for issue open or description changes', () => { + expect(shouldTrackAgentActivity(execution(), 'issue')).toBe(true); + expect(shouldTrackAgentActivity(execution({ issue: { number: 7, opened: false, descriptionEdited: false } }), 'issue')).toBe(false); + }); + + it('tracks agent-backed pull request review events', () => { + expect(shouldTrackAgentActivity(execution({ + eventName: 'pull_request', + issueNumber: -1, + issue: { number: -1, opened: false, descriptionEdited: false }, + pullRequest: { number: 12, action: 'synchronize', commentBody: '' }, + }), 'pull-request')).toBe(true); + }); + + it('tracks non-empty issue and review comments when an agent is configured', () => { + expect(shouldTrackAgentActivity(execution({ + eventName: 'issue_comment', + issue: { number: 7, commentBody: 'Please review this' }, + }), 'issue-comment')).toBe(true); + expect(shouldTrackAgentActivity(execution({ + eventName: 'pull_request_review_comment', + issueNumber: -1, + issue: { number: -1, commentBody: '' }, + pullRequest: { number: 12, action: 'created', commentBody: 'Please fix this' }, + }), 'pull-request-review-comment')).toBe(true); + }); + + it('tracks pull request description work when the planner is available', () => { + const unavailable = { model: '', command: '' }; + const available = { model: 'model', command: 'agent' }; + expect(shouldTrackAgentActivity(execution({ + eventName: 'pull_request', + issueNumber: -1, + issue: { number: -1 }, + pullRequest: { number: 12, action: 'edited', commentBody: '' }, + ai: { + getAgentConfiguration: jest.fn((task: string) => task === 'planner' ? available : unavailable), + getAiPullRequestDescription: jest.fn(() => true), + }, + }), 'pull-request')).toBe(true); + }); + + it('tracks push analysis only when commits and findings configuration exist', () => { + expect(shouldTrackAgentActivity(execution({ + eventName: 'push', + commit: { commits: [{ id: 'commit-1' }] }, + }), 'push')).toBe(true); + expect(shouldTrackAgentActivity(execution({ + eventName: 'push', + commit: { commits: [] }, + }), 'push')).toBe(false); + }); + + it('tracks only supported agent-backed single actions', () => { + expect(shouldTrackAgentActivity(execution({ + isSingleAction: true, + singleAction: { isThinkAction: true }, + }), 'single-action')).toBe(true); + expect(shouldTrackAgentActivity(execution({ + isSingleAction: true, + singleAction: { isCheckProgressAction: true }, + }), 'single-action')).toBe(true); + expect(shouldTrackAgentActivity(execution({ + isSingleAction: true, + singleAction: {}, + }), 'single-action')).toBe(false); + }); + + it('does not track routes without an effective agent configuration', () => { + expect(shouldTrackAgentActivity(execution({ + ai: { + getAgentConfiguration: jest.fn(() => ({ model: '', command: '' })), + getAiPullRequestDescription: jest.fn(() => false), + }, + }), 'issue')).toBe(false); + }); + + it('does not track a route with no issue or pull request target', () => { + expect(shouldTrackAgentActivity(execution({ issueNumber: -1, issue: { number: -1 } }), 'issue')).toBe(false); + }); +}); diff --git a/src/application/policies/__tests__/initial_label_provisioning_policy.test.ts b/src/application/policies/__tests__/initial_label_provisioning_policy.test.ts index a3c0d4b5..878e92d6 100644 --- a/src/application/policies/__tests__/initial_label_provisioning_policy.test.ts +++ b/src/application/policies/__tests__/initial_label_provisioning_policy.test.ts @@ -22,7 +22,7 @@ describe('initial label provisioning policy', () => { [], ); - expect(plan.configured.missing).toHaveLength(33); + expect(plan.configured.missing).toHaveLength(35); expect(plan.configured).toEqual({ existing: 0, missing: expect.arrayContaining([ @@ -32,7 +32,7 @@ describe('initial label provisioning policy', () => { description: 'Label to trigger branch management actions', }, { - name: 'copilot:state:planned', + name: 'state:planned', color: '1D76DB', description: 'Copilot has produced an implementation plan.', }, @@ -63,20 +63,23 @@ describe('initial label provisioning policy', () => { ['EXISTING'], ); + expect(plan.configured.missing).toHaveLength(12); expect(plan.configured).toEqual({ existing: 1, - missing: [ + missing: expect.arrayContaining([ expect.objectContaining({ name: '0%' }), expect.objectContaining({ name: 'new' }), - expect.objectContaining({ name: 'copilot:state:analyzing' }), - expect.objectContaining({ name: 'copilot:state:planned' }), - expect.objectContaining({ name: 'copilot:state:in-progress' }), - expect.objectContaining({ name: 'copilot:state:reviewing' }), - expect.objectContaining({ name: 'copilot:state:changes-requested' }), - expect.objectContaining({ name: 'copilot:state:verified' }), - expect.objectContaining({ name: 'copilot:state:ready' }), - expect.objectContaining({ name: 'copilot:state:blocked' }), - ], + expect.objectContaining({ name: 'state:ai-processing' }), + expect.objectContaining({ name: 'state:planned' }), + expect.objectContaining({ name: 'state:in-progress' }), + expect.objectContaining({ name: 'state:reviewing' }), + expect.objectContaining({ name: 'state:changes-requested' }), + expect.objectContaining({ name: 'state:verified' }), + expect.objectContaining({ name: 'state:ready' }), + expect.objectContaining({ name: 'state:blocked' }), + expect.objectContaining({ name: 'state:awaiting-maintainer' }), + expect.objectContaining({ name: 'state:awaiting-issue-author' }), + ]), }); expect(plan.progress.existing).toBe(0); expect(plan.progress.missing).toHaveLength(20); diff --git a/src/application/policies/__tests__/lifecycle_state_policy.test.ts b/src/application/policies/__tests__/lifecycle_state_policy.test.ts index c7ea087d..dd321c8a 100644 --- a/src/application/policies/__tests__/lifecycle_state_policy.test.ts +++ b/src/application/policies/__tests__/lifecycle_state_policy.test.ts @@ -3,8 +3,8 @@ import { resolveLifecycleState } from '../lifecycle_state_policy'; const result = (id: string, success = true) => ({ id, success, executed: true, steps: [], errors: [] }); describe('lifecycle state policy', () => { - it('moves an issue to analyzing, planned, and in-progress based on route facts', () => { - expect(resolveLifecycleState({ eventName: 'issues', action: 'opened', isIssue: true, isPullRequest: false, issueOpened: true, issueDescriptionEdited: false, pullRequestMerged: false, pullRequestClosed: false, results: [] })).toBe('analyzing'); + it('moves an issue to planned and in-progress while agent activity remains separate', () => { + expect(resolveLifecycleState({ eventName: 'issues', action: 'opened', isIssue: true, isPullRequest: false, issueOpened: true, issueDescriptionEdited: false, pullRequestMerged: false, pullRequestClosed: false, results: [] })).toBeUndefined(); expect(resolveLifecycleState({ eventName: 'issues', action: 'edited', isIssue: true, isPullRequest: false, issueOpened: false, issueDescriptionEdited: true, pullRequestMerged: false, pullRequestClosed: false, results: [result('RecommendStepsUseCase')] })).toBe('planned'); expect(resolveLifecycleState({ eventName: 'issues', action: 'labeled', isIssue: true, isPullRequest: false, issueOpened: false, issueDescriptionEdited: false, pullRequestMerged: false, pullRequestClosed: false, results: [result('PrepareBranchesUseCase')] })).toBe('in-progress'); }); diff --git a/src/application/policies/__tests__/lifecycle_waiting_state_policy.test.ts b/src/application/policies/__tests__/lifecycle_waiting_state_policy.test.ts new file mode 100644 index 00000000..5fcb5b2d --- /dev/null +++ b/src/application/policies/__tests__/lifecycle_waiting_state_policy.test.ts @@ -0,0 +1,27 @@ +import { resolveLifecycleWaitingState } from '../lifecycle_waiting_state_policy'; + +describe('lifecycle waiting state policy', () => { + it.each([ + ['planned', 'awaiting-maintainer'], + ['ready', 'awaiting-maintainer'], + ['blocked', 'awaiting-maintainer'], + ['changes-requested', 'awaiting-issue-author'], + ] as const)('maps %s to %s', (lifecycleState, waitingState) => { + expect(resolveLifecycleWaitingState({ eventName: 'issues', lifecycleState })).toEqual({ + kind: 'set', + state: waitingState, + }); + }); + + it('clears waiting state when a route reaches another stable state', () => { + expect(resolveLifecycleWaitingState({ eventName: 'issues', lifecycleState: 'in-progress' })).toEqual({ kind: 'clear' }); + }); + + it('clears waiting state after a human interaction without a new stable state', () => { + expect(resolveLifecycleWaitingState({ eventName: 'issue_comment', lifecycleState: undefined })).toEqual({ kind: 'clear' }); + }); + + it('preserves waiting state for an unrelated lifecycle event', () => { + expect(resolveLifecycleWaitingState({ eventName: 'workflow_dispatch', lifecycleState: undefined })).toEqual({ kind: 'preserve' }); + }); +}); diff --git a/src/application/policies/agent_activity_label_policy.ts b/src/application/policies/agent_activity_label_policy.ts new file mode 100644 index 00000000..530be93b --- /dev/null +++ b/src/application/policies/agent_activity_label_policy.ts @@ -0,0 +1,12 @@ +/** Adds or removes one activity label without touching unrelated labels. */ +export function replaceAgentActivityLabel( + currentLabels: readonly string[], + activityLabel: string, + active: boolean, +): string[] { + const normalizedActivityLabel = activityLabel.trim().toLowerCase(); + if (!normalizedActivityLabel) return [...currentLabels]; + + const retained = currentLabels.filter(label => label.trim().toLowerCase() !== normalizedActivityLabel); + return active ? [...retained, activityLabel] : retained; +} diff --git a/src/application/policies/agent_activity_policy.ts b/src/application/policies/agent_activity_policy.ts new file mode 100644 index 00000000..f67db8f4 --- /dev/null +++ b/src/application/policies/agent_activity_policy.ts @@ -0,0 +1,65 @@ +import type { Execution } from '../../data/model/execution'; +import { isAgentConfigurationReady } from '../../domain/agent'; + +export type AgentActivityRoute = + | 'single-action' + | 'issue-comment' + | 'issue' + | 'pull-request-review-comment' + | 'pull-request' + | 'push'; + +/** Decides whether a route can invoke an agent for its current event. */ +export function shouldTrackAgentActivity( + execution: Execution, + route: AgentActivityRoute, +): boolean { + if (!hasTarget(execution)) return false; + + switch (route) { + case 'issue': + return (execution.issue.opened || execution.issue.descriptionEdited) + && isAgentReady(execution, 'planner'); + case 'issue-comment': + case 'pull-request-review-comment': + return hasComment(execution) + && (isAgentReady(execution, 'planner') + || isAgentReady(execution, 'findings') + || isAgentReady(execution, 'fixer')); + case 'pull-request': + return ['opened', 'reopened', 'edited', 'synchronize'].includes(execution.pullRequest.action) + && (isAgentReady(execution, 'reviewer') + || (execution.ai.getAiPullRequestDescription() && isAgentReady(execution, 'planner'))); + case 'push': + return execution.commit.commits.length > 0 && isAgentReady(execution, 'findings'); + case 'single-action': + return isAgentBackedSingleAction(execution); + default: + return false; + } +} + +function isAgentBackedSingleAction(execution: Execution): boolean { + if (execution.singleAction.isThinkAction || execution.singleAction.isRecommendStepsAction) { + return isAgentReady(execution, 'planner'); + } + if (execution.singleAction.isCheckProgressAction || execution.singleAction.isDetectPotentialProblemsAction) { + return isAgentReady(execution, 'findings'); + } + return false; +} + +function isAgentReady(execution: Execution, task: Parameters[0]): boolean { + return isAgentConfigurationReady(execution.ai?.getAgentConfiguration(task)); +} + +function hasComment(execution: Execution): boolean { + return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; +} + +function hasTarget(execution: Execution): boolean { + if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + return execution.pullRequest.number > 0; + } + return execution.issue.number > 0 || execution.issueNumber > 0; +} diff --git a/src/application/policies/initial_label_provisioning_policy.ts b/src/application/policies/initial_label_provisioning_policy.ts index 5b52c007..8e0edda1 100644 --- a/src/application/policies/initial_label_provisioning_policy.ts +++ b/src/application/policies/initial_label_provisioning_policy.ts @@ -3,7 +3,7 @@ import { PROGRESS_LABEL_PERCENTS, progressPercentToColor, } from './progress_labels'; -import { lifecycleLabelDefinitions } from '../../domain/copilot_lifecycle'; +import { managedLifecycleLabelDefinitions } from '../../domain/copilot_lifecycle'; export interface InitialLabelDefinition { name: string; @@ -65,7 +65,7 @@ function progressLabelDefinitions(): InitialLabelDefinition[] { } function lifecycleLabelDefinitionsFor(labels: Labels): InitialLabelDefinition[] { - return lifecycleLabelDefinitions(labels.lifecycle).map(definition => ({ + return managedLifecycleLabelDefinitions(labels.lifecycle).map(definition => ({ name: definition.name, color: definition.color, description: definition.description, diff --git a/src/application/policies/lifecycle_state_policy.ts b/src/application/policies/lifecycle_state_policy.ts index f6d9b429..0160a5d3 100644 --- a/src/application/policies/lifecycle_state_policy.ts +++ b/src/application/policies/lifecycle_state_policy.ts @@ -43,7 +43,6 @@ export function resolveLifecycleState( if (hasResult(input.results, 'PrepareBranchesUseCase')) return 'in-progress'; if (hasSuccessfulResult(input.results, 'RecommendStepsUseCase')) return 'planned'; if (hasExplicitPlanningCommand(input.results)) return 'planned'; - if (input.issueOpened || input.issueDescriptionEdited) return 'analyzing'; return undefined; } diff --git a/src/application/policies/lifecycle_waiting_state_policy.ts b/src/application/policies/lifecycle_waiting_state_policy.ts new file mode 100644 index 00000000..90f04b6a --- /dev/null +++ b/src/application/policies/lifecycle_waiting_state_policy.ts @@ -0,0 +1,42 @@ +import type { CopilotLifecycleState, CopilotWaitingState } from '../../domain/copilot_lifecycle'; + +export type LifecycleWaitingStateDecision = + | { kind: 'set'; state: CopilotWaitingState } + | { kind: 'clear' } + | { kind: 'preserve' }; + +export interface LifecycleWaitingStateInput { + readonly eventName: string; + readonly lifecycleState: CopilotLifecycleState | undefined; +} + +/** + * Resolves who should provide the next human input. Waiting labels are + * orthogonal to the stable lifecycle phase and at most one is retained. + */ +export function resolveLifecycleWaitingState( + input: LifecycleWaitingStateInput, +): LifecycleWaitingStateDecision { + if (input.lifecycleState === 'planned' + || input.lifecycleState === 'ready' + || input.lifecycleState === 'blocked') { + return { kind: 'set', state: 'awaiting-maintainer' }; + } + if (input.lifecycleState === 'changes-requested') { + return { kind: 'set', state: 'awaiting-issue-author' }; + } + if (input.lifecycleState !== undefined || isHumanInteraction(input.eventName)) { + return { kind: 'clear' }; + } + return { kind: 'preserve' }; +} + +function isHumanInteraction(eventName: string): boolean { + return [ + 'issues', + 'issue_comment', + 'pull_request', + 'pull_request_review_comment', + 'push', + ].includes(eventName); +} diff --git a/src/application/usecases/actions/__tests__/synchronize_agent_activity_use_case.test.ts b/src/application/usecases/actions/__tests__/synchronize_agent_activity_use_case.test.ts new file mode 100644 index 00000000..8183e5f4 --- /dev/null +++ b/src/application/usecases/actions/__tests__/synchronize_agent_activity_use_case.test.ts @@ -0,0 +1,101 @@ +import { SynchronizeAgentActivityUseCase } from '../synchronize_agent_activity_use_case'; + +function execution(overrides: Record = {}): any { + return { + owner: 'owner', + repo: 'repo', + eventName: 'issues', + issueNumber: 7, + issue: { number: 7 }, + pullRequest: { number: 0 }, + labels: { + currentIssueLabels: ['feature', 'state:in-progress', 'state:awaiting-maintainer'], + currentPullRequestLabels: [], + lifecycle: { + aiProcessing: 'state:ai-processing', + planned: 'state:planned', + inProgress: 'state:in-progress', + reviewing: 'state:reviewing', + changesRequested: 'state:changes-requested', + verified: 'state:verified', + ready: 'state:ready', + blocked: 'state:blocked', + awaitingMaintainer: 'state:awaiting-maintainer', + awaitingIssueAuthor: 'state:awaiting-issue-author', + }, + }, + tokens: { token: 'token' }, + ...overrides, + }; +} + +describe('SynchronizeAgentActivityUseCase', () => { + it('adds and removes the activity label while preserving other labels', async () => { + const setLabels = jest.fn().mockResolvedValue(undefined); + const getLabels = jest.fn().mockResolvedValue([ + 'feature', + 'state:in-progress', + 'state:awaiting-maintainer', + 'state:ai-processing', + 'size: M', + ]); + const param = execution(); + const useCase = new SynchronizeAgentActivityUseCase({ setLabels, getLabels }); + + await useCase.start(param); + await useCase.finish(param); + + expect(setLabels).toHaveBeenNthCalledWith( + 1, + 'owner', + 'repo', + 7, + ['feature', 'state:in-progress', 'state:awaiting-maintainer', 'state:ai-processing'], + 'token', + ); + expect(setLabels).toHaveBeenNthCalledWith( + 2, + 'owner', + 'repo', + 7, + ['feature', 'state:in-progress', 'state:awaiting-maintainer', 'size: M'], + 'token', + ); + expect(getLabels).toHaveBeenCalledWith('owner', 'repo', 7, 'token'); + }); + + it('keeps route execution best-effort when label synchronization fails', async () => { + const setLabels = jest.fn().mockRejectedValue(new Error('labels unavailable')); + const getLabels = jest.fn().mockRejectedValue(new Error('labels unavailable')); + const useCase = new SynchronizeAgentActivityUseCase({ setLabels, getLabels }); + + await expect(useCase.start(execution())).resolves.toBeUndefined(); + await expect(useCase.finish(execution())).resolves.toBeUndefined(); + }); + + it('targets pull request labels for pull request review comments', async () => { + const setLabels = jest.fn().mockResolvedValue(undefined); + const param = execution({ + eventName: 'pull_request_review_comment', + issueNumber: -1, + issue: { number: -1 }, + pullRequest: { number: 11 }, + labels: { + ...execution().labels, + currentIssueLabels: [], + currentPullRequestLabels: ['state:reviewing'], + }, + }); + const useCase = new SynchronizeAgentActivityUseCase({ setLabels, getLabels: jest.fn() }); + + await useCase.start(param); + + expect(setLabels).toHaveBeenCalledWith( + 'owner', + 'repo', + 11, + ['state:reviewing', 'state:ai-processing'], + 'token', + ); + }); +}); diff --git a/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts b/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts index fa1cf889..2fb6d4cd 100644 --- a/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts @@ -10,11 +10,12 @@ function execution(overrides: Record = {}): Execution { issue: { number: 7, opened: true, descriptionEdited: false }, pullRequest: { number: 0, isMerged: false, isClosed: false }, labels: { - currentIssueLabels: ['bug', 'copilot:state:ready'], + currentIssueLabels: ['bug', 'state:ready'], currentPullRequestLabels: [], lifecycle: { - analyzing: 'copilot:state:analyzing', planned: 'copilot:state:planned', inProgress: 'copilot:state:in-progress', - reviewing: 'copilot:state:reviewing', changesRequested: 'copilot:state:changes-requested', verified: 'copilot:state:verified', ready: 'copilot:state:ready', blocked: 'copilot:state:blocked', + aiProcessing: 'state:ai-processing', planned: 'state:planned', inProgress: 'state:in-progress', + reviewing: 'state:reviewing', changesRequested: 'state:changes-requested', verified: 'state:verified', ready: 'state:ready', blocked: 'state:blocked', + awaitingMaintainer: 'state:awaiting-maintainer', awaitingIssueAuthor: 'state:awaiting-issue-author', }, }, tokens: { token: 'token' }, @@ -28,18 +29,126 @@ describe('SynchronizeLifecycleStateUseCase', () => { const useCase = new SynchronizeLifecycleStateUseCase({ setLabels, getLabels: jest.fn() }); const param = execution(); - const results = await useCase.invoke({ execution: param, results: [] }); + const results = await useCase.invoke({ + execution: param, + results: [{ id: 'RecommendStepsUseCase', success: true, executed: true, steps: [], errors: [] } as never], + }); - expect(setLabels).toHaveBeenCalledWith('owner', 'repo', 7, ['bug', 'copilot:state:analyzing'], 'token'); + expect(setLabels).toHaveBeenCalledWith('owner', 'repo', 7, ['bug', 'state:planned', 'state:awaiting-maintainer'], 'token'); expect(results[0]).toMatchObject({ success: true, executed: true }); }); it('does not write when the state is already current', async () => { const setLabels = jest.fn(); const useCase = new SynchronizeLifecycleStateUseCase({ setLabels, getLabels: jest.fn() }); - const param = execution({ labels: { ...execution().labels, currentIssueLabels: ['bug', 'copilot:state:analyzing'] } }); + const param = execution({ labels: { ...execution().labels, currentIssueLabels: ['bug', 'state:ai-processing'] } }); expect(await useCase.invoke({ execution: param, results: [] })).toEqual([]); expect(setLabels).not.toHaveBeenCalled(); }); + + it('preserves agent activity while synchronizing the stable state', async () => { + const setLabels = jest.fn().mockResolvedValue(undefined); + const getLabels = jest.fn().mockResolvedValue(['bug', 'state:ai-processing', 'state:ready', 'size: M']); + const useCase = new SynchronizeLifecycleStateUseCase({ setLabels, getLabels }); + const param = execution({ + labels: { + ...execution().labels, + currentIssueLabels: ['bug', 'state:ai-processing', 'state:ready'], + }, + issue: { number: 7, opened: false, descriptionEdited: true }, + inputs: { action: 'edited' }, + }); + + await useCase.invoke({ + execution: param, + results: [{ id: 'PrepareBranchesUseCase', success: true, executed: true, steps: [], errors: [] } as never], + }); + + expect(setLabels).toHaveBeenCalledWith( + 'owner', + 'repo', + 7, + ['bug', 'state:ai-processing', 'size: M', 'state:in-progress'], + 'token', + ); + }); + + it('does not migrate legacy copilot-prefixed labels automatically', async () => { + const setLabels = jest.fn().mockResolvedValue(undefined); + const useCase = new SynchronizeLifecycleStateUseCase({ setLabels, getLabels: jest.fn() }); + const param = execution({ + labels: { + ...execution().labels, + currentIssueLabels: ['bug', 'copilot:state:ready'], + }, + }); + + await useCase.invoke({ + execution: param, + results: [{ id: 'RecommendStepsUseCase', success: true, executed: true, steps: [], errors: [] } as never], + }); + + expect(setLabels).toHaveBeenCalledWith( + 'owner', + 'repo', + 7, + ['bug', 'copilot:state:ready', 'state:planned', 'state:awaiting-maintainer'], + 'token', + ); + }); + + it('maps active findings to the issue-author waiting label', async () => { + const setLabels = jest.fn().mockResolvedValue(undefined); + const useCase = new SynchronizeLifecycleStateUseCase({ setLabels, getLabels: jest.fn() }); + const param = execution({ + eventName: 'pull_request', + inputs: { action: 'synchronize' }, + pullRequest: { number: 11, isMerged: false, isClosed: false }, + labels: { + ...execution().labels, + currentIssueLabels: [], + currentPullRequestLabels: ['state:ai-processing', 'state:reviewing'], + }, + }); + + await useCase.invoke({ + execution: param, + results: [{ id: 'DetectPotentialProblemsUseCase', success: true, executed: true, steps: [], errors: [], payload: { findingStates: { open: 1, reopened: 0 } } } as never], + }); + + expect(setLabels).toHaveBeenCalledWith( + 'owner', + 'repo', + 11, + ['state:ai-processing', 'state:changes-requested', 'state:awaiting-issue-author'], + 'token', + ); + }); + + it('clears a waiting label when a pull request review comment arrives', async () => { + const setLabels = jest.fn().mockResolvedValue(undefined); + const useCase = new SynchronizeLifecycleStateUseCase({ setLabels, getLabels: jest.fn() }); + const param = execution({ + eventName: 'pull_request_review_comment', + inputs: { action: 'created' }, + issue: { number: -1, opened: false, descriptionEdited: false }, + pullRequest: { number: 11, isMerged: false, isClosed: false }, + labels: { + ...execution().labels, + currentIssueLabels: [], + currentPullRequestLabels: ['state:changes-requested', 'state:awaiting-issue-author'], + }, + }); + + await useCase.invoke({ execution: param, results: [] }); + + expect(setLabels).toHaveBeenCalledWith( + 'owner', + 'repo', + 11, + ['state:changes-requested'], + 'token', + ); + }); }); diff --git a/src/application/usecases/actions/synchronize_agent_activity_use_case.ts b/src/application/usecases/actions/synchronize_agent_activity_use_case.ts new file mode 100644 index 00000000..0985634a --- /dev/null +++ b/src/application/usecases/actions/synchronize_agent_activity_use_case.ts @@ -0,0 +1,92 @@ +import type { Execution } from '../../../data/model/execution'; +import { activityLabel } from '../../../domain/copilot_lifecycle'; +import { replaceAgentActivityLabel } from '../../policies/agent_activity_label_policy'; +import type { IssueLabelsPort } from '../../ports/issue_management_ports'; +import { logDebugInfo, logError, logInfo } from '../../ports/logging_ports'; + +/** + * Maintains the temporary agent-activity label around a complete route. + * Cleanup is deliberately best-effort so a label outage never hides the + * actual route result; the in-memory execution remains synchronized after a + * successful mutation so later lifecycle writes preserve the activity label. + */ +export class SynchronizeAgentActivityUseCase { + readonly taskId = 'SynchronizeAgentActivityUseCase'; + + constructor(private readonly issueLabelsPort: IssueLabelsPort) {} + + async start(execution: Execution): Promise { + await this.synchronize(execution, true); + } + + async finish(execution: Execution): Promise { + await this.synchronize(execution, false); + } + + private async synchronize(execution: Execution, active: boolean): Promise { + const target = resolveTarget(execution); + if (!target) { + logDebugInfo(`${this.taskId}: no issue or pull request target; skipping activity label.`); + return; + } + + try { + // Route steps may have changed labels through their own ports. Read + // the latest server inventory before cleanup so removing the + // transient marker cannot overwrite those changes. + const currentLabels = active + ? target.labels + : await this.issueLabelsPort.getLabels( + execution.owner, + execution.repo, + target.number, + execution.tokens.token, + ); + const configuredLabel = activityLabel(execution.labels.lifecycle); + const nextLabels = replaceAgentActivityLabel(currentLabels, configuredLabel, active); + if (sameLabels(currentLabels, nextLabels)) return; + + await this.issueLabelsPort.setLabels( + execution.owner, + execution.repo, + target.number, + nextLabels, + execution.tokens.token, + ); + target.setLabels(nextLabels); + logInfo(`${active ? 'Added' : 'Removed'} Copilot agent activity label on target #${target.number}.`); + } catch (error) { + const message = `${this.taskId}: unable to ${active ? 'add' : 'remove'} agent activity label.`; + logError(message, error instanceof Error ? { stack: error.stack } : undefined); + } + } +} + +interface LabelTarget { + number: number; + labels: string[]; + setLabels: (labels: string[]) => void; +} + +function resolveTarget(execution: Execution): LabelTarget | undefined { + if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + if (execution.pullRequest.number <= 0) return undefined; + return { + number: execution.pullRequest.number, + labels: execution.labels.currentPullRequestLabels, + setLabels: labels => { execution.labels.currentPullRequestLabels = labels; }, + }; + } + + const number = execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; + if (number <= 0) return undefined; + return { + number, + labels: execution.labels.currentIssueLabels, + setLabels: labels => { execution.labels.currentIssueLabels = labels; }, + }; +} + +function sameLabels(left: readonly string[], right: readonly string[]): boolean { + return left.length === right.length && left.every((label, index) => label === right[index]); +} diff --git a/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts b/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts index 7cec8518..e9486c4a 100644 --- a/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts +++ b/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts @@ -1,7 +1,8 @@ import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; -import { lifecycleLabelNames, lifecycleStateLabel } from '../../../domain/copilot_lifecycle'; +import { lifecycleLabelNames, lifecycleStateLabel, waitingLabelNames, waitingStateLabel } from '../../../domain/copilot_lifecycle'; import { resolveLifecycleState } from '../../policies/lifecycle_state_policy'; +import { resolveLifecycleWaitingState, type LifecycleWaitingStateDecision } from '../../policies/lifecycle_waiting_state_policy'; import type { IssueLabelsPort } from '../../ports/issue_management_ports'; import { logDebugInfo, logError } from '../../ports/logging_ports'; @@ -24,14 +25,17 @@ export class SynchronizeLifecycleStateUseCase { eventName: param.execution.eventName, action: param.execution.inputs?.action ?? '', isIssue: ['issues', 'issue_comment'].includes(param.execution.eventName), - isPullRequest: param.execution.eventName === 'pull_request', + isPullRequest: ['pull_request', 'pull_request_review_comment'].includes(param.execution.eventName), issueOpened: param.execution.issue.opened, issueDescriptionEdited: param.execution.issue.descriptionEdited, pullRequestMerged: param.execution.pullRequest.isMerged, pullRequestClosed: param.execution.pullRequest.isClosed, results: param.results, }); - if (!state) return []; + const waitingDecision = resolveLifecycleWaitingState({ + eventName: param.execution.eventName, + lifecycleState: state, + }); const issueNumber = targetNumber(param.execution); if (issueNumber <= 0) { @@ -39,24 +43,37 @@ export class SynchronizeLifecycleStateUseCase { return []; } - const currentLabels = targetLabels(param.execution); - const nextLabels = replaceLifecycleLabels(currentLabels, state, param.execution.labels.lifecycle); - if (sameLabels(currentLabels, nextLabels)) return []; - try { - await this.issueLabelsPort.setLabels( + // Route steps may have changed labels through their own ports. Use + // the latest server inventory before reconciliation so this + // use case cannot overwrite those changes with setup-time data. + const currentLabels = await this.issueLabelsPort.getLabels( param.execution.owner, param.execution.repo, issueNumber, + param.execution.tokens.token, + ) ?? targetLabels(param.execution); + const nextLabels = replaceLifecycleLabels(currentLabels, state, param.execution.labels.lifecycle); + const nextLabelsWithWaiting = replaceWaitingLabels( nextLabels, + waitingDecision, + param.execution.labels.lifecycle, + ); + if (sameLabels(currentLabels, nextLabelsWithWaiting)) return []; + + await this.issueLabelsPort.setLabels( + param.execution.owner, + param.execution.repo, + issueNumber, + nextLabelsWithWaiting, param.execution.tokens.token, ); - setTargetLabels(param.execution, nextLabels); + setTargetLabels(param.execution, nextLabelsWithWaiting); return [new Result({ id: this.taskId, success: true, executed: true, - steps: [`Lifecycle state synchronized to \`${state}\`.`], + steps: lifecycleSynchronizationSteps(state, waitingDecision), })]; } catch (error) { const message = `Unable to synchronize Copilot lifecycle state: ${error instanceof Error ? error.message : String(error)}`; @@ -67,33 +84,64 @@ export class SynchronizeLifecycleStateUseCase { } function targetNumber(execution: Execution): number { - if (['issues', 'issue_comment'].includes(execution.eventName)) return execution.issue.number; - if (execution.eventName === 'pull_request') return execution.pullRequest.number; + if (['issues', 'issue_comment', 'push'].includes(execution.eventName)) { + return execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; + } + if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) return execution.pullRequest.number; return -1; } function targetLabels(execution: Execution): string[] { - return execution.eventName === 'pull_request' + return ['pull_request', 'pull_request_review_comment'].includes(execution.eventName) ? execution.labels.currentPullRequestLabels : execution.labels.currentIssueLabels; } function setTargetLabels(execution: Execution, labels: string[]): void { - if (execution.eventName === 'pull_request') execution.labels.currentPullRequestLabels = labels; + if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) { + execution.labels.currentPullRequestLabels = labels; + } else execution.labels.currentIssueLabels = labels; } function replaceLifecycleLabels( currentLabels: readonly string[], - state: Parameters[0], + state: Parameters[0] | undefined, lifecycleLabels: Parameters[0], ): string[] { + if (!state) return [...currentLabels]; const managedLabels = new Set(lifecycleLabelNames(lifecycleLabels).map(label => label.toLowerCase())); const retained = currentLabels.filter(label => !managedLabels.has(label.trim().toLowerCase())); const next = lifecycleStateLabel(state, lifecycleLabels); return [...retained, next]; } +function replaceWaitingLabels( + currentLabels: readonly string[], + decision: LifecycleWaitingStateDecision, + lifecycleLabels: Parameters[0], +): string[] { + if (decision.kind === 'preserve') return [...currentLabels]; + const managedLabels = new Set(waitingLabelNames(lifecycleLabels).map(label => label.toLowerCase())); + const retained = currentLabels.filter(label => !managedLabels.has(label.trim().toLowerCase())); + if (decision.kind === 'clear') return retained; + return [...retained, waitingStateLabel(decision.state, lifecycleLabels)]; +} + +function lifecycleSynchronizationSteps( + state: Parameters[0] | undefined, + waitingDecision: LifecycleWaitingStateDecision, +): string[] { + const steps: string[] = []; + if (state) steps.push(`Lifecycle state synchronized to \`${state}\`.`); + if (waitingDecision.kind === 'set') { + steps.push(`Waiting state synchronized to \`${waitingDecision.state}\`.`); + } else if (waitingDecision.kind === 'clear') { + steps.push('Waiting state cleared.'); + } + return steps; +} + function sameLabels(left: readonly string[], right: readonly string[]): boolean { return left.length === right.length && left.every((label, index) => label === right[index]); } diff --git a/src/data/repository/issue/__tests__/issue_label_provisioning_repository.test.ts b/src/data/repository/issue/__tests__/issue_label_provisioning_repository.test.ts index 73c22f73..671ab81f 100644 --- a/src/data/repository/issue/__tests__/issue_label_provisioning_repository.test.ts +++ b/src/data/repository/issue/__tests__/issue_label_provisioning_repository.test.ts @@ -42,7 +42,7 @@ describe('IssueLabelProvisioningRepository', () => { await expect( repository.ensureInitialLabels('owner', 'repo', labels, 'token'), ).resolves.toEqual({ - configured: { created: 9, existing: 1, errors: [] }, + configured: { created: 11, existing: 1, errors: [] }, progress: { created: 21, existing: 0, errors: [] }, }); @@ -53,7 +53,7 @@ describe('IssueLabelProvisioningRepository', () => { repo: 'repo', per_page: 100, }); - expect(createLabel).toHaveBeenCalledTimes(30); + expect(createLabel).toHaveBeenCalledTimes(32); expect(createLabel).not.toHaveBeenCalledWith( expect.objectContaining({ name: 'existing' }), ); @@ -95,10 +95,10 @@ describe('IssueLabelProvisioningRepository', () => { 'token', ), ).resolves.toEqual({ - configured: { created: 8, existing: 1, errors: [] }, + configured: { created: 10, existing: 1, errors: [] }, progress: { created: 21, existing: 0, errors: [] }, }); - expect(createLabel).toHaveBeenCalledTimes(30); + expect(createLabel).toHaveBeenCalledTimes(32); }); it('serializes provider mutations', async () => { @@ -139,7 +139,7 @@ describe('IssueLabelProvisioningRepository', () => { expect(createLabel).toHaveBeenCalledTimes(1); releaseFirstMutation(); await provisioning; - expect(createLabel).toHaveBeenCalledTimes(31); + expect(createLabel).toHaveBeenCalledTimes(33); }); it('aggregates provider errors by category and continues with remaining labels', async () => { @@ -168,7 +168,7 @@ describe('IssueLabelProvisioningRepository', () => { ), ).resolves.toEqual({ configured: { - created: 9, + created: 11, existing: 0, errors: ['Error creating label "bug": bug unavailable'], }, @@ -178,6 +178,6 @@ describe('IssueLabelProvisioningRepository', () => { errors: ['Error creating label "10%": progress unavailable'], }, }); - expect(createLabel).toHaveBeenCalledTimes(31); + expect(createLabel).toHaveBeenCalledTimes(33); }); }); diff --git a/src/domain/__tests__/copilot_lifecycle.test.ts b/src/domain/__tests__/copilot_lifecycle.test.ts index 831655f8..466d0849 100644 --- a/src/domain/__tests__/copilot_lifecycle.test.ts +++ b/src/domain/__tests__/copilot_lifecycle.test.ts @@ -1,22 +1,35 @@ import { DEFAULT_COPILOT_LIFECYCLE_LABELS, + activityLabel, lifecycleLabelDefinitions, lifecycleStateFromLabels, lifecycleStateLabel, + managedLifecycleLabelDefinitions, + waitingStateLabel, } from '../copilot_lifecycle'; describe('Copilot lifecycle policy', () => { it('provides a complete, unique default label catalog', () => { const definitions = lifecycleLabelDefinitions(); - expect(definitions).toHaveLength(8); + expect(definitions).toHaveLength(7); expect(new Set(definitions.map(definition => definition.name)).size).toBe(definitions.length); expect(definitions.map(definition => definition.name)).toContain(DEFAULT_COPILOT_LIFECYCLE_LABELS.ready); }); - it('maps state to labels and labels back to state case-insensitively', () => { + it('keeps activity and waiting labels outside the stable lifecycle state', () => { + const definitions = managedLifecycleLabelDefinitions(); + expect(definitions).toHaveLength(10); + expect(activityLabel()).toBe('state:ai-processing'); + expect(waitingStateLabel('awaiting-maintainer')).toBe('state:awaiting-maintainer'); + expect(definitions.find(definition => definition.name === activityLabel())).toMatchObject({ category: 'activity' }); + expect(definitions.find(definition => definition.name === waitingStateLabel('awaiting-issue-author'))).toMatchObject({ category: 'waiting' }); + }); + + it('maps stable state to labels and labels back to state case-insensitively', () => { const label = lifecycleStateLabel('changes-requested'); expect(lifecycleStateFromLabels(['bug', label.toUpperCase()])).toBe('changes-requested'); expect(lifecycleStateFromLabels(['bug'])).toBeUndefined(); + expect(lifecycleStateFromLabels(['state:ai-processing'])).toBeUndefined(); + expect(lifecycleStateFromLabels(['copilot:state:ready'])).toBeUndefined(); }); }); - diff --git a/src/domain/copilot_lifecycle.ts b/src/domain/copilot_lifecycle.ts index 71cea335..b4c0041d 100644 --- a/src/domain/copilot_lifecycle.ts +++ b/src/domain/copilot_lifecycle.ts @@ -1,10 +1,10 @@ /** - * The Copilot lifecycle is deliberately independent from GitHub's API model. - * Labels are the persistence representation; this policy is the state machine - * used by application workflows and can therefore be tested without I/O. + * Copilot labels are split into independent dimensions. A durable lifecycle + * phase can coexist with temporary agent activity and a human waiting state. + * This policy is independent from GitHub's API model and remains unit-testable + * without I/O. */ export type CopilotLifecycleState = - | 'analyzing' | 'planned' | 'in-progress' | 'reviewing' @@ -13,8 +13,12 @@ export type CopilotLifecycleState = | 'ready' | 'blocked'; +export type CopilotAgentActivity = 'ai-processing'; + +export type CopilotWaitingState = 'awaiting-maintainer' | 'awaiting-issue-author'; + export interface CopilotLifecycleLabels { - analyzing: string; + aiProcessing: string; planned: string; inProgress: string; reviewing: string; @@ -22,28 +26,38 @@ export interface CopilotLifecycleLabels { verified: string; ready: string; blocked: string; + awaitingMaintainer: string; + awaitingIssueAuthor: string; } export const DEFAULT_COPILOT_LIFECYCLE_LABELS: Readonly = { - analyzing: 'copilot:state:analyzing', - planned: 'copilot:state:planned', - inProgress: 'copilot:state:in-progress', - reviewing: 'copilot:state:reviewing', - changesRequested: 'copilot:state:changes-requested', - verified: 'copilot:state:verified', - ready: 'copilot:state:ready', - blocked: 'copilot:state:blocked', + aiProcessing: 'state:ai-processing', + planned: 'state:planned', + inProgress: 'state:in-progress', + reviewing: 'state:reviewing', + changesRequested: 'state:changes-requested', + verified: 'state:verified', + ready: 'state:ready', + blocked: 'state:blocked', + awaitingMaintainer: 'state:awaiting-maintainer', + awaitingIssueAuthor: 'state:awaiting-issue-author', }; +export type LifecycleLabelCategory = 'lifecycle' | 'activity' | 'waiting'; + export interface LifecycleLabelDefinition { - readonly state: CopilotLifecycleState; + readonly category: LifecycleLabelCategory; + readonly state?: CopilotLifecycleState; readonly name: string; readonly color: string; readonly description: string; } -const LIFECYCLE_METADATA: ReadonlyArray = [ - ['analyzing', 'analyzing', 'FBCA04', 'Copilot is analyzing the issue or change.'], +type StableMetadata = readonly [CopilotLifecycleState, keyof CopilotLifecycleLabels, string, string]; +type ActivityMetadata = readonly [CopilotAgentActivity, keyof CopilotLifecycleLabels, string, string]; +type WaitingMetadata = readonly [CopilotWaitingState, keyof CopilotLifecycleLabels, string, string]; + +const STABLE_LIFECYCLE_METADATA: ReadonlyArray = [ ['planned', 'planned', '1D76DB', 'Copilot has produced an implementation plan.'], ['in-progress', 'inProgress', '0E8A16', 'Implementation work is in progress.'], ['reviewing', 'reviewing', '5319E7', 'A pull request is being reviewed.'], @@ -53,10 +67,18 @@ const LIFECYCLE_METADATA: ReadonlyArray ({ +const ACTIVITY_METADATA: ReadonlyArray = [ + ['ai-processing', 'aiProcessing', 'FBCA04', 'A Copilot agent is analyzing or working on the issue or change.'], +]; + +const WAITING_METADATA: ReadonlyArray = [ + ['awaiting-maintainer', 'awaitingMaintainer', '5319E7', 'The next action requires a maintainer response or approval.'], + ['awaiting-issue-author', 'awaitingIssueAuthor', 'D93F0B', 'The next action requires more information or changes from the issue author.'], +]; + +function stableDefinitions(labels: CopilotLifecycleLabels): LifecycleLabelDefinition[] { + return STABLE_LIFECYCLE_METADATA.map(([state, key, color, description]) => ({ + category: 'lifecycle', state, name: labels[key], color, @@ -64,12 +86,76 @@ export function lifecycleLabelDefinitions( })); } +function activityDefinitions(labels: CopilotLifecycleLabels): LifecycleLabelDefinition[] { + return ACTIVITY_METADATA.map(([, key, color, description]) => ({ + category: 'activity', + name: labels[key], + color, + description, + })); +} + +function waitingDefinitions(labels: CopilotLifecycleLabels): LifecycleLabelDefinition[] { + return WAITING_METADATA.map(([, key, color, description]) => ({ + category: 'waiting', + name: labels[key], + color, + description, + })); +} + +export function lifecycleLabelDefinitions( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): LifecycleLabelDefinition[] { + return stableDefinitions(labels); +} + +export function activityLabelDefinitions( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): LifecycleLabelDefinition[] { + return activityDefinitions(labels); +} + +export function waitingLabelDefinitions( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): LifecycleLabelDefinition[] { + return waitingDefinitions(labels); +} + +export function managedLifecycleLabelDefinitions( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): LifecycleLabelDefinition[] { + return [ + ...stableDefinitions(labels), + ...activityDefinitions(labels), + ...waitingDefinitions(labels), + ]; +} + export function lifecycleLabelNames( labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, ): string[] { return lifecycleLabelDefinitions(labels).map(definition => definition.name); } +export function activityLabelNames( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): string[] { + return activityLabelDefinitions(labels).map(definition => definition.name); +} + +export function waitingLabelNames( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): string[] { + return waitingLabelDefinitions(labels).map(definition => definition.name); +} + +export function managedLifecycleLabelNames( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): string[] { + return managedLifecycleLabelDefinitions(labels).map(definition => definition.name); +} + export function lifecycleStateLabel( state: CopilotLifecycleState, labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, @@ -79,6 +165,21 @@ export function lifecycleStateLabel( return definition.name; } +export function activityLabel( + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): string { + return labels.aiProcessing; +} + +export function waitingStateLabel( + state: CopilotWaitingState, + labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, +): string { + const metadata = WAITING_METADATA.find(([metadataState]) => metadataState === state); + if (!metadata) throw new Error(`Unknown Copilot waiting state: ${state}`); + return labels[metadata[1]]; +} + export function lifecycleStateFromLabels( currentLabels: readonly string[], labels: CopilotLifecycleLabels = DEFAULT_COPILOT_LIFECYCLE_LABELS, @@ -86,4 +187,3 @@ export function lifecycleStateFromLabels( const normalized = new Set(currentLabels.map(label => label.trim().toLowerCase())); return lifecycleLabelDefinitions(labels).find(definition => normalized.has(definition.name.trim().toLowerCase()))?.state; } - diff --git a/src/infrastructure/composition/agent_activity_composition_root.ts b/src/infrastructure/composition/agent_activity_composition_root.ts new file mode 100644 index 00000000..da961a09 --- /dev/null +++ b/src/infrastructure/composition/agent_activity_composition_root.ts @@ -0,0 +1,6 @@ +import { SynchronizeAgentActivityUseCase } from '../../application/usecases/actions/synchronize_agent_activity_use_case'; +import { createIssueLabelRepository } from './issue_labels_composition_root'; + +export function createSynchronizeAgentActivityUseCase(): SynchronizeAgentActivityUseCase { + return new SynchronizeAgentActivityUseCase(createIssueLabelRepository()); +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cedc328c..1f410380 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -335,15 +335,17 @@ export const INPUT_KEYS = { SIZE_S_LABEL: 'size-s-label', SIZE_XS_LABEL: 'size-xs-label', - // Copilot lifecycle labels - COPILOT_STATE_ANALYZING_LABEL: 'copilot-state-analyzing-label', - COPILOT_STATE_PLANNED_LABEL: 'copilot-state-planned-label', - COPILOT_STATE_IN_PROGRESS_LABEL: 'copilot-state-in-progress-label', - COPILOT_STATE_REVIEWING_LABEL: 'copilot-state-reviewing-label', - COPILOT_STATE_CHANGES_REQUESTED_LABEL: 'copilot-state-changes-requested-label', - COPILOT_STATE_VERIFIED_LABEL: 'copilot-state-verified-label', - COPILOT_STATE_READY_LABEL: 'copilot-state-ready-label', - COPILOT_STATE_BLOCKED_LABEL: 'copilot-state-blocked-label', + // Lifecycle label inputs + STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', + STATE_PLANNED_LABEL: 'state-planned-label', + STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', + STATE_REVIEWING_LABEL: 'state-reviewing-label', + STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', + STATE_VERIFIED_LABEL: 'state-verified-label', + STATE_READY_LABEL: 'state-ready-label', + STATE_BLOCKED_LABEL: 'state-blocked-label', + STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', + STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', // Issue Types ISSUE_TYPE_BUG: 'issue-type-bug', From 13a37c89fc00352412c6e77377fb3e92f2841ce8 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 2 Sep 2026 19:50:22 +0200 Subject: [PATCH 02/11] master: harden setup and add credential doctor --- README.md | 4 +- build/cli/index.js | 4925 +++++++++++++++-- .../policies/setup_configuration_policy.d.ts | 23 + .../application/ports/setup_wizard_ports.d.ts | 53 + .../ports/setup_workspace_ports.d.ts | 11 +- .../actions/initial_setup_use_case.d.ts | 5 +- .../actions/initial_setup_workflow.d.ts | 3 + .../usecases/setup/doctor_use_case.d.ts | 19 + .../src/application/usecases/setup/index.d.ts | 4 + .../setup/setup_credentials_use_case.d.ts | 24 + .../usecases/setup/setup_wizard_use_case.d.ts | 14 + build/cli/src/cli/commands/doctor.d.ts | 2 + build/cli/src/cli/commands/setup_policy.d.ts | 3 +- build/cli/src/cli/setup_config_file.d.ts | 3 + build/cli/src/cli/setup_prompt_adapter.d.ts | 32 + .../repository_variables_repository.d.ts | 31 + build/cli/src/domain/setup.d.ts | 110 + .../github_identity_client_factory.d.ts | 2 + .../setup_credentials_composition_root.d.ts | 3 + .../setup_doctor_composition_root.d.ts | 3 + .../octokit_credential_health_adapter.d.ts | 5 + .../octokit_repository_variables_adapter.d.ts | 5 + .../github_credential_health_protocol.d.ts | 52 + .../github_repository_variables_protocol.d.ts | 36 + .../setup_credential_validation_adapter.d.ts | 18 + ...etup_remote_credential_health_adapter.d.ts | 25 + .../setup_workspace_adapter.d.ts | 8 +- build/cli/src/utils/setup_file_copy.d.ts | 8 +- build/cli/src/utils/setup_files.d.ts | 18 +- build/github_action/index.js | 3162 ++++++++++- .../policies/setup_configuration_policy.d.ts | 23 + .../application/ports/setup_wizard_ports.d.ts | 53 + .../ports/setup_workspace_ports.d.ts | 11 +- .../actions/initial_setup_use_case.d.ts | 5 +- .../actions/initial_setup_workflow.d.ts | 3 + .../usecases/setup/doctor_use_case.d.ts | 19 + .../src/application/usecases/setup/index.d.ts | 4 + .../setup/setup_credentials_use_case.d.ts | 24 + .../usecases/setup/setup_wizard_use_case.d.ts | 14 + .../src/cli/commands/doctor.d.ts | 2 + .../src/cli/commands/setup_policy.d.ts | 3 +- .../src/cli/setup_config_file.d.ts | 3 + .../src/cli/setup_prompt_adapter.d.ts | 32 + .../repository_variables_repository.d.ts | 31 + build/github_action/src/domain/setup.d.ts | 110 + .../github_identity_client_factory.d.ts | 2 + .../setup_credentials_composition_root.d.ts | 3 + .../setup_doctor_composition_root.d.ts | 3 + .../octokit_credential_health_adapter.d.ts | 5 + .../octokit_repository_variables_adapter.d.ts | 5 + .../github_credential_health_protocol.d.ts | 52 + .../github_repository_variables_protocol.d.ts | 36 + .../setup_credential_validation_adapter.d.ts | 18 + ...etup_remote_credential_health_adapter.d.ts | 25 + .../setup_workspace_adapter.d.ts | 8 +- .../src/utils/setup_file_copy.d.ts | 8 +- .../github_action/src/utils/setup_files.d.ts | 18 +- docs/authentication.mdx | 29 +- docs/bugbot/detection.mdx | 2 +- docs/bugbot/examples.mdx | 4 +- docs/development/architecture.mdx | 7 + docs/development/local-development.mdx | 2 +- docs/development/testing.mdx | 4 + docs/how-to-use.mdx | 35 +- docs/single-actions/examples.mdx | 2 +- docs/single-actions/workflow-and-cli.mdx | 94 +- package.json | 5 +- pnpm-lock.yaml | 537 +- setup/.env | 1 - setup/workflows/agent-cli-provisioning.yml | 52 +- setup/workflows/copilot_commit.yml | 27 + setup/workflows/copilot_credential_health.yml | 170 + setup/workflows/copilot_issue.yml | 29 +- setup/workflows/copilot_issue_comment.yml | 29 +- setup/workflows/copilot_pull_request.yml | 28 +- .../copilot_pull_request_comment.yml | 29 +- setup/workflows/hotfix_workflow.yml | 12 + setup/workflows/release_workflow.yml | 12 + src/__tests__/cli.test.ts | 17 +- .../setup_configuration_policy.test.ts | 107 + .../policies/setup_configuration_policy.ts | 390 ++ src/application/ports/setup_wizard_ports.ts | 72 + .../ports/setup_workspace_ports.ts | 13 +- .../__tests__/initial_setup_use_case.test.ts | 24 +- .../actions/initial_setup_use_case.ts | 5 + .../actions/initial_setup_workflow.ts | 123 +- .../setup/__tests__/doctor_use_case.test.ts | 86 + .../setup_credentials_use_case.test.ts | 75 + .../__tests__/setup_wizard_use_case.test.ts | 45 + .../usecases/setup/doctor_use_case.ts | 80 + src/application/usecases/setup/index.ts | 4 + .../setup/setup_credentials_use_case.ts | 109 + .../usecases/setup/setup_wizard_use_case.ts | 48 + src/cli/__tests__/setup_config_file.test.ts | 77 + .../__tests__/setup_prompt_adapter.test.ts | 28 + src/cli/cli_program.ts | 3 - src/cli/command_registry.ts | 2 + src/cli/commands/doctor.ts | 44 + src/cli/commands/setup.ts | 172 +- src/cli/commands/setup_policy.ts | 11 +- src/cli/setup_config_file.ts | 140 + src/cli/setup_prompt_adapter.ts | 349 ++ .../__tests__/issue_type_repository.test.ts | 8 +- .../repository_variables_repository.test.ts | 63 + .../issue/issue_type_ensure_workflow.ts | 2 +- .../repository/issue/issue_type_queries.ts | 8 +- .../repository_variables_repository.ts | 108 + src/domain/setup.ts | 139 + ...etup_credential_validation_adapter.test.ts | 72 + ...p_remote_credential_health_adapter.test.ts | 104 + .../initial_setup_composition_root.test.ts | 4 +- .../github_identity_client_factory.ts | 2 + .../initial_setup_composition_root.ts | 5 + .../setup_credentials_composition_root.ts | 17 + .../setup_doctor_composition_root.ts | 20 + .../octokit_credential_health_adapter.ts | 9 + .../octokit_repository_variables_adapter.ts | 9 + .../github_credential_health_protocol.ts | 30 + .../github_repository_variables_protocol.ts | 25 + .../setup_credential_validation_adapter.ts | 134 + .../setup_remote_credential_health_adapter.ts | 178 + src/infrastructure/setup_workspace_adapter.ts | 22 +- src/utils/__tests__/setup_files.test.ts | 327 +- src/utils/setup_file_copy.ts | 17 +- src/utils/setup_files.ts | 128 +- 125 files changed, 12320 insertions(+), 1376 deletions(-) create mode 100644 build/cli/src/application/policies/setup_configuration_policy.d.ts create mode 100644 build/cli/src/application/ports/setup_wizard_ports.d.ts create mode 100644 build/cli/src/application/usecases/setup/doctor_use_case.d.ts create mode 100644 build/cli/src/application/usecases/setup/index.d.ts create mode 100644 build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts create mode 100644 build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts create mode 100644 build/cli/src/cli/commands/doctor.d.ts create mode 100644 build/cli/src/cli/setup_config_file.d.ts create mode 100644 build/cli/src/cli/setup_prompt_adapter.d.ts create mode 100644 build/cli/src/data/repository/repository_variables_repository.d.ts create mode 100644 build/cli/src/domain/setup.d.ts create mode 100644 build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts create mode 100644 build/cli/src/infrastructure/composition/setup_doctor_composition_root.d.ts create mode 100644 build/cli/src/infrastructure/github/octokit_credential_health_adapter.d.ts create mode 100644 build/cli/src/infrastructure/github/octokit_repository_variables_adapter.d.ts create mode 100644 build/cli/src/infrastructure/github/ports/github_credential_health_protocol.d.ts create mode 100644 build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts create mode 100644 build/cli/src/infrastructure/setup_credential_validation_adapter.d.ts create mode 100644 build/cli/src/infrastructure/setup_remote_credential_health_adapter.d.ts create mode 100644 build/github_action/src/application/policies/setup_configuration_policy.d.ts create mode 100644 build/github_action/src/application/ports/setup_wizard_ports.d.ts create mode 100644 build/github_action/src/application/usecases/setup/doctor_use_case.d.ts create mode 100644 build/github_action/src/application/usecases/setup/index.d.ts create mode 100644 build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts create mode 100644 build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts create mode 100644 build/github_action/src/cli/commands/doctor.d.ts create mode 100644 build/github_action/src/cli/setup_config_file.d.ts create mode 100644 build/github_action/src/cli/setup_prompt_adapter.d.ts create mode 100644 build/github_action/src/data/repository/repository_variables_repository.d.ts create mode 100644 build/github_action/src/domain/setup.d.ts create mode 100644 build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts create mode 100644 build/github_action/src/infrastructure/composition/setup_doctor_composition_root.d.ts create mode 100644 build/github_action/src/infrastructure/github/octokit_credential_health_adapter.d.ts create mode 100644 build/github_action/src/infrastructure/github/octokit_repository_variables_adapter.d.ts create mode 100644 build/github_action/src/infrastructure/github/ports/github_credential_health_protocol.d.ts create mode 100644 build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts create mode 100644 build/github_action/src/infrastructure/setup_credential_validation_adapter.d.ts create mode 100644 build/github_action/src/infrastructure/setup_remote_credential_health_adapter.d.ts delete mode 100644 setup/.env create mode 100644 setup/workflows/copilot_credential_health.yml create mode 100644 src/application/policies/__tests__/setup_configuration_policy.test.ts create mode 100644 src/application/policies/setup_configuration_policy.ts create mode 100644 src/application/ports/setup_wizard_ports.ts create mode 100644 src/application/usecases/setup/__tests__/doctor_use_case.test.ts create mode 100644 src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts create mode 100644 src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts create mode 100644 src/application/usecases/setup/doctor_use_case.ts create mode 100644 src/application/usecases/setup/index.ts create mode 100644 src/application/usecases/setup/setup_credentials_use_case.ts create mode 100644 src/application/usecases/setup/setup_wizard_use_case.ts create mode 100644 src/cli/__tests__/setup_config_file.test.ts create mode 100644 src/cli/__tests__/setup_prompt_adapter.test.ts create mode 100644 src/cli/commands/doctor.ts create mode 100644 src/cli/setup_config_file.ts create mode 100644 src/cli/setup_prompt_adapter.ts create mode 100644 src/data/repository/__tests__/repository_variables_repository.test.ts create mode 100644 src/data/repository/repository_variables_repository.ts create mode 100644 src/domain/setup.ts create mode 100644 src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts create mode 100644 src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts create mode 100644 src/infrastructure/composition/setup_credentials_composition_root.ts create mode 100644 src/infrastructure/composition/setup_doctor_composition_root.ts create mode 100644 src/infrastructure/github/octokit_credential_health_adapter.ts create mode 100644 src/infrastructure/github/octokit_repository_variables_adapter.ts create mode 100644 src/infrastructure/github/ports/github_credential_health_protocol.ts create mode 100644 src/infrastructure/github/ports/github_repository_variables_protocol.ts create mode 100644 src/infrastructure/setup_credential_validation_adapter.ts create mode 100644 src/infrastructure/setup_remote_credential_health_adapter.ts diff --git a/README.md b/README.md index 09838530..770e4368 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Full documentation: **[docs.page/vypdev/copilot](https://docs.page/vypdev/copilo ## Getting started -1. **Create a PAT** and store it as a repo secret (e.g. `PAT`). See [Authentication](https://docs.page/vypdev/copilot/authentication). +1. **Create the workflow PAT** for the bot account and store it as a repo secret (e.g. `PAT`). `copilot setup` separately asks the operator for a setup PAT that is used only during local configuration. See [Authentication](https://docs.page/vypdev/copilot/authentication). 2. **Use the action** from the marketplace so versions are stable: ```yaml uses: vypdev/copilot@v3 @@ -41,7 +41,7 @@ Full documentation: **[docs.page/vypdev/copilot](https://docs.page/vypdev/copilo copilot --version ``` Update the published CLI later with **`copilot upgrade`**. -4. **Add workflows** — Copy the files from `setup/workflows/` into your `.github/workflows/`, or run **`copilot setup`** from your repo root (with `PERSONAL_ACCESS_TOKEN` in `.env`). See [How to use](https://docs.page/vypdev/copilot/how-to-use). +4. **Add workflows** — Copy the files from `setup/workflows/` into your `.github/workflows/`, or run **`copilot setup`** from your repo root. The setup wizard securely prompts for its separate operator PAT and can validate/provision the workflow PAT and provider credentials. See [How to use](https://docs.page/vypdev/copilot/how-to-use). --- diff --git a/build/cli/index.js b/build/cli/index.js index 079bb77c..c2fd53b8 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -19164,399 +19164,6 @@ class Deprecation extends Error { exports.Deprecation = Deprecation; -/***/ }), - -/***/ 11406: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -const fs = __nccwpck_require__(57147) -const path = __nccwpck_require__(71017) -const os = __nccwpck_require__(22037) -const crypto = __nccwpck_require__(6113) -const packageJson = __nccwpck_require__(92655) - -const version = packageJson.version - -const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg - -// Parse src into an Object -function parse (src) { - const obj = {} - - // Convert buffer to string - let lines = src.toString() - - // Convert line breaks to same format - lines = lines.replace(/\r\n?/mg, '\n') - - let match - while ((match = LINE.exec(lines)) != null) { - const key = match[1] - - // Default undefined or null to empty string - let value = (match[2] || '') - - // Remove whitespace - value = value.trim() - - // Check if double quoted - const maybeQuote = value[0] - - // Remove surrounding quotes - value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2') - - // Expand newlines if double quoted - if (maybeQuote === '"') { - value = value.replace(/\\n/g, '\n') - value = value.replace(/\\r/g, '\r') - } - - // Add to object - obj[key] = value - } - - return obj -} - -function _parseVault (options) { - options = options || {} - - const vaultPath = _vaultPath(options) - options.path = vaultPath // parse .env.vault - const result = DotenvModule.configDotenv(options) - if (!result.parsed) { - const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`) - err.code = 'MISSING_DATA' - throw err - } - - // handle scenario for comma separated keys - for use with key rotation - // example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod" - const keys = _dotenvKey(options).split(',') - const length = keys.length - - let decrypted - for (let i = 0; i < length; i++) { - try { - // Get full key - const key = keys[i].trim() - - // Get instructions for decrypt - const attrs = _instructions(result, key) - - // Decrypt - decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key) - - break - } catch (error) { - // last key - if (i + 1 >= length) { - throw error - } - // try next key - } - } - - // Parse decrypted .env string - return DotenvModule.parse(decrypted) -} - -function _warn (message) { - console.log(`[dotenv@${version}][WARN] ${message}`) -} - -function _debug (message) { - console.log(`[dotenv@${version}][DEBUG] ${message}`) -} - -function _log (message) { - console.log(`[dotenv@${version}] ${message}`) -} - -function _dotenvKey (options) { - // prioritize developer directly setting options.DOTENV_KEY - if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { - return options.DOTENV_KEY - } - - // secondary infra already contains a DOTENV_KEY environment variable - if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { - return process.env.DOTENV_KEY - } - - // fallback to empty string - return '' -} - -function _instructions (result, dotenvKey) { - // Parse DOTENV_KEY. Format is a URI - let uri - try { - uri = new URL(dotenvKey) - } catch (error) { - if (error.code === 'ERR_INVALID_URL') { - const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development') - err.code = 'INVALID_DOTENV_KEY' - throw err - } - - throw error - } - - // Get decrypt key - const key = uri.password - if (!key) { - const err = new Error('INVALID_DOTENV_KEY: Missing key part') - err.code = 'INVALID_DOTENV_KEY' - throw err - } - - // Get environment - const environment = uri.searchParams.get('environment') - if (!environment) { - const err = new Error('INVALID_DOTENV_KEY: Missing environment part') - err.code = 'INVALID_DOTENV_KEY' - throw err - } - - // Get ciphertext payload - const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}` - const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION - if (!ciphertext) { - const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`) - err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT' - throw err - } - - return { ciphertext, key } -} - -function _vaultPath (options) { - let possibleVaultPath = null - - if (options && options.path && options.path.length > 0) { - if (Array.isArray(options.path)) { - for (const filepath of options.path) { - if (fs.existsSync(filepath)) { - possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault` - } - } - } else { - possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault` - } - } else { - possibleVaultPath = path.resolve(process.cwd(), '.env.vault') - } - - if (fs.existsSync(possibleVaultPath)) { - return possibleVaultPath - } - - return null -} - -function _resolveHome (envPath) { - return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath -} - -function _configVault (options) { - const debug = Boolean(options && options.debug) - const quiet = options && 'quiet' in options ? options.quiet : true - - if (debug || !quiet) { - _log('Loading env from encrypted .env.vault') - } - - const parsed = DotenvModule._parseVault(options) - - let processEnv = process.env - if (options && options.processEnv != null) { - processEnv = options.processEnv - } - - DotenvModule.populate(processEnv, parsed, options) - - return { parsed } -} - -function configDotenv (options) { - const dotenvPath = path.resolve(process.cwd(), '.env') - let encoding = 'utf8' - const debug = Boolean(options && options.debug) - const quiet = options && 'quiet' in options ? options.quiet : true - - if (options && options.encoding) { - encoding = options.encoding - } else { - if (debug) { - _debug('No encoding is specified. UTF-8 is used by default') - } - } - - let optionPaths = [dotenvPath] // default, look for .env - if (options && options.path) { - if (!Array.isArray(options.path)) { - optionPaths = [_resolveHome(options.path)] - } else { - optionPaths = [] // reset default - for (const filepath of options.path) { - optionPaths.push(_resolveHome(filepath)) - } - } - } - - // Build the parsed data in a temporary object (because we need to return it). Once we have the final - // parsed data, we will combine it with process.env (or options.processEnv if provided). - let lastError - const parsedAll = {} - for (const path of optionPaths) { - try { - // Specifying an encoding returns a string instead of a buffer - const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding })) - - DotenvModule.populate(parsedAll, parsed, options) - } catch (e) { - if (debug) { - _debug(`Failed to load ${path} ${e.message}`) - } - lastError = e - } - } - - let processEnv = process.env - if (options && options.processEnv != null) { - processEnv = options.processEnv - } - - DotenvModule.populate(processEnv, parsedAll, options) - - if (debug || !quiet) { - const keysCount = Object.keys(parsedAll).length - const shortPaths = [] - for (const filePath of optionPaths) { - try { - const relative = path.relative(process.cwd(), filePath) - shortPaths.push(relative) - } catch (e) { - if (debug) { - _debug(`Failed to load ${filePath} ${e.message}`) - } - lastError = e - } - } - - _log(`injecting env (${keysCount}) from ${shortPaths.join(',')}`) - } - - if (lastError) { - return { parsed: parsedAll, error: lastError } - } else { - return { parsed: parsedAll } - } -} - -// Populates process.env from .env file -function config (options) { - // fallback to original dotenv if DOTENV_KEY is not set - if (_dotenvKey(options).length === 0) { - return DotenvModule.configDotenv(options) - } - - const vaultPath = _vaultPath(options) - - // dotenvKey exists but .env.vault file does not exist - if (!vaultPath) { - _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`) - - return DotenvModule.configDotenv(options) - } - - return DotenvModule._configVault(options) -} - -function decrypt (encrypted, keyStr) { - const key = Buffer.from(keyStr.slice(-64), 'hex') - let ciphertext = Buffer.from(encrypted, 'base64') - - const nonce = ciphertext.subarray(0, 12) - const authTag = ciphertext.subarray(-16) - ciphertext = ciphertext.subarray(12, -16) - - try { - const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce) - aesgcm.setAuthTag(authTag) - return `${aesgcm.update(ciphertext)}${aesgcm.final()}` - } catch (error) { - const isRange = error instanceof RangeError - const invalidKeyLength = error.message === 'Invalid key length' - const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data' - - if (isRange || invalidKeyLength) { - const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)') - err.code = 'INVALID_DOTENV_KEY' - throw err - } else if (decryptionFailed) { - const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY') - err.code = 'DECRYPTION_FAILED' - throw err - } else { - throw error - } - } -} - -// Populate process.env with parsed values -function populate (processEnv, parsed, options = {}) { - const debug = Boolean(options && options.debug) - const override = Boolean(options && options.override) - - if (typeof parsed !== 'object') { - const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate') - err.code = 'OBJECT_REQUIRED' - throw err - } - - // Set process.env - for (const key of Object.keys(parsed)) { - if (Object.prototype.hasOwnProperty.call(processEnv, key)) { - if (override === true) { - processEnv[key] = parsed[key] - } - - if (debug) { - if (override === true) { - _debug(`"${key}" is already defined and WAS overwritten`) - } else { - _debug(`"${key}" is already defined and was NOT overwritten`) - } - } - } else { - processEnv[key] = parsed[key] - } - } -} - -const DotenvModule = { - configDotenv, - _configVault, - _parseVault, - config, - decrypt, - parse, - populate -} - -module.exports.configDotenv = DotenvModule.configDotenv -module.exports._configVault = DotenvModule._configVault -module.exports._parseVault = DotenvModule._parseVault -module.exports.config = DotenvModule.config -module.exports.decrypt = DotenvModule.decrypt -module.exports.parse = DotenvModule.parse -module.exports.populate = DotenvModule.populate - -module.exports = DotenvModule - - /***/ }), /***/ 33104: @@ -24543,6 +24150,2404 @@ if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { exports.debug = debug; // for test +/***/ }), + +/***/ 24258: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +(function(nacl) { +'use strict'; + +// Ported in 2014 by Dmitry Chestnykh and Devi Mandiri. +// Public domain. +// +// Implementation derived from TweetNaCl version 20140427. +// See for details: http://tweetnacl.cr.yp.to/ + +var gf = function(init) { + var i, r = new Float64Array(16); + if (init) for (i = 0; i < init.length; i++) r[i] = init[i]; + return r; +}; + +// Pluggable, initialized in high-level API below. +var randombytes = function(/* x, n */) { throw new Error('no PRNG'); }; + +var _0 = new Uint8Array(16); +var _9 = new Uint8Array(32); _9[0] = 9; + +var gf0 = gf(), + gf1 = gf([1]), + _121665 = gf([0xdb41, 1]), + D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]), + D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]), + X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]), + Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]), + I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]); + +function ts64(x, i, h, l) { + x[i] = (h >> 24) & 0xff; + x[i+1] = (h >> 16) & 0xff; + x[i+2] = (h >> 8) & 0xff; + x[i+3] = h & 0xff; + x[i+4] = (l >> 24) & 0xff; + x[i+5] = (l >> 16) & 0xff; + x[i+6] = (l >> 8) & 0xff; + x[i+7] = l & 0xff; +} + +function vn(x, xi, y, yi, n) { + var i,d = 0; + for (i = 0; i < n; i++) d |= x[xi+i]^y[yi+i]; + return (1 & ((d - 1) >>> 8)) - 1; +} + +function crypto_verify_16(x, xi, y, yi) { + return vn(x,xi,y,yi,16); +} + +function crypto_verify_32(x, xi, y, yi) { + return vn(x,xi,y,yi,32); +} + +function core_salsa20(o, p, k, c) { + var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; + + var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u; + + for (var i = 0; i < 20; i += 2) { + u = x0 + x12 | 0; + x4 ^= u<<7 | u>>>(32-7); + u = x4 + x0 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x4 | 0; + x12 ^= u<<13 | u>>>(32-13); + u = x12 + x8 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x1 | 0; + x9 ^= u<<7 | u>>>(32-7); + u = x9 + x5 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x9 | 0; + x1 ^= u<<13 | u>>>(32-13); + u = x1 + x13 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x6 | 0; + x14 ^= u<<7 | u>>>(32-7); + u = x14 + x10 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x14 | 0; + x6 ^= u<<13 | u>>>(32-13); + u = x6 + x2 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x11 | 0; + x3 ^= u<<7 | u>>>(32-7); + u = x3 + x15 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x3 | 0; + x11 ^= u<<13 | u>>>(32-13); + u = x11 + x7 | 0; + x15 ^= u<<18 | u>>>(32-18); + + u = x0 + x3 | 0; + x1 ^= u<<7 | u>>>(32-7); + u = x1 + x0 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x1 | 0; + x3 ^= u<<13 | u>>>(32-13); + u = x3 + x2 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x4 | 0; + x6 ^= u<<7 | u>>>(32-7); + u = x6 + x5 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x6 | 0; + x4 ^= u<<13 | u>>>(32-13); + u = x4 + x7 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x9 | 0; + x11 ^= u<<7 | u>>>(32-7); + u = x11 + x10 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x11 | 0; + x9 ^= u<<13 | u>>>(32-13); + u = x9 + x8 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x14 | 0; + x12 ^= u<<7 | u>>>(32-7); + u = x12 + x15 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x12 | 0; + x14 ^= u<<13 | u>>>(32-13); + u = x14 + x13 | 0; + x15 ^= u<<18 | u>>>(32-18); + } + x0 = x0 + j0 | 0; + x1 = x1 + j1 | 0; + x2 = x2 + j2 | 0; + x3 = x3 + j3 | 0; + x4 = x4 + j4 | 0; + x5 = x5 + j5 | 0; + x6 = x6 + j6 | 0; + x7 = x7 + j7 | 0; + x8 = x8 + j8 | 0; + x9 = x9 + j9 | 0; + x10 = x10 + j10 | 0; + x11 = x11 + j11 | 0; + x12 = x12 + j12 | 0; + x13 = x13 + j13 | 0; + x14 = x14 + j14 | 0; + x15 = x15 + j15 | 0; + + o[ 0] = x0 >>> 0 & 0xff; + o[ 1] = x0 >>> 8 & 0xff; + o[ 2] = x0 >>> 16 & 0xff; + o[ 3] = x0 >>> 24 & 0xff; + + o[ 4] = x1 >>> 0 & 0xff; + o[ 5] = x1 >>> 8 & 0xff; + o[ 6] = x1 >>> 16 & 0xff; + o[ 7] = x1 >>> 24 & 0xff; + + o[ 8] = x2 >>> 0 & 0xff; + o[ 9] = x2 >>> 8 & 0xff; + o[10] = x2 >>> 16 & 0xff; + o[11] = x2 >>> 24 & 0xff; + + o[12] = x3 >>> 0 & 0xff; + o[13] = x3 >>> 8 & 0xff; + o[14] = x3 >>> 16 & 0xff; + o[15] = x3 >>> 24 & 0xff; + + o[16] = x4 >>> 0 & 0xff; + o[17] = x4 >>> 8 & 0xff; + o[18] = x4 >>> 16 & 0xff; + o[19] = x4 >>> 24 & 0xff; + + o[20] = x5 >>> 0 & 0xff; + o[21] = x5 >>> 8 & 0xff; + o[22] = x5 >>> 16 & 0xff; + o[23] = x5 >>> 24 & 0xff; + + o[24] = x6 >>> 0 & 0xff; + o[25] = x6 >>> 8 & 0xff; + o[26] = x6 >>> 16 & 0xff; + o[27] = x6 >>> 24 & 0xff; + + o[28] = x7 >>> 0 & 0xff; + o[29] = x7 >>> 8 & 0xff; + o[30] = x7 >>> 16 & 0xff; + o[31] = x7 >>> 24 & 0xff; + + o[32] = x8 >>> 0 & 0xff; + o[33] = x8 >>> 8 & 0xff; + o[34] = x8 >>> 16 & 0xff; + o[35] = x8 >>> 24 & 0xff; + + o[36] = x9 >>> 0 & 0xff; + o[37] = x9 >>> 8 & 0xff; + o[38] = x9 >>> 16 & 0xff; + o[39] = x9 >>> 24 & 0xff; + + o[40] = x10 >>> 0 & 0xff; + o[41] = x10 >>> 8 & 0xff; + o[42] = x10 >>> 16 & 0xff; + o[43] = x10 >>> 24 & 0xff; + + o[44] = x11 >>> 0 & 0xff; + o[45] = x11 >>> 8 & 0xff; + o[46] = x11 >>> 16 & 0xff; + o[47] = x11 >>> 24 & 0xff; + + o[48] = x12 >>> 0 & 0xff; + o[49] = x12 >>> 8 & 0xff; + o[50] = x12 >>> 16 & 0xff; + o[51] = x12 >>> 24 & 0xff; + + o[52] = x13 >>> 0 & 0xff; + o[53] = x13 >>> 8 & 0xff; + o[54] = x13 >>> 16 & 0xff; + o[55] = x13 >>> 24 & 0xff; + + o[56] = x14 >>> 0 & 0xff; + o[57] = x14 >>> 8 & 0xff; + o[58] = x14 >>> 16 & 0xff; + o[59] = x14 >>> 24 & 0xff; + + o[60] = x15 >>> 0 & 0xff; + o[61] = x15 >>> 8 & 0xff; + o[62] = x15 >>> 16 & 0xff; + o[63] = x15 >>> 24 & 0xff; +} + +function core_hsalsa20(o,p,k,c) { + var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; + + var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u; + + for (var i = 0; i < 20; i += 2) { + u = x0 + x12 | 0; + x4 ^= u<<7 | u>>>(32-7); + u = x4 + x0 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x4 | 0; + x12 ^= u<<13 | u>>>(32-13); + u = x12 + x8 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x1 | 0; + x9 ^= u<<7 | u>>>(32-7); + u = x9 + x5 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x9 | 0; + x1 ^= u<<13 | u>>>(32-13); + u = x1 + x13 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x6 | 0; + x14 ^= u<<7 | u>>>(32-7); + u = x14 + x10 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x14 | 0; + x6 ^= u<<13 | u>>>(32-13); + u = x6 + x2 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x11 | 0; + x3 ^= u<<7 | u>>>(32-7); + u = x3 + x15 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x3 | 0; + x11 ^= u<<13 | u>>>(32-13); + u = x11 + x7 | 0; + x15 ^= u<<18 | u>>>(32-18); + + u = x0 + x3 | 0; + x1 ^= u<<7 | u>>>(32-7); + u = x1 + x0 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x1 | 0; + x3 ^= u<<13 | u>>>(32-13); + u = x3 + x2 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x4 | 0; + x6 ^= u<<7 | u>>>(32-7); + u = x6 + x5 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x6 | 0; + x4 ^= u<<13 | u>>>(32-13); + u = x4 + x7 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x9 | 0; + x11 ^= u<<7 | u>>>(32-7); + u = x11 + x10 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x11 | 0; + x9 ^= u<<13 | u>>>(32-13); + u = x9 + x8 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x14 | 0; + x12 ^= u<<7 | u>>>(32-7); + u = x12 + x15 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x12 | 0; + x14 ^= u<<13 | u>>>(32-13); + u = x14 + x13 | 0; + x15 ^= u<<18 | u>>>(32-18); + } + + o[ 0] = x0 >>> 0 & 0xff; + o[ 1] = x0 >>> 8 & 0xff; + o[ 2] = x0 >>> 16 & 0xff; + o[ 3] = x0 >>> 24 & 0xff; + + o[ 4] = x5 >>> 0 & 0xff; + o[ 5] = x5 >>> 8 & 0xff; + o[ 6] = x5 >>> 16 & 0xff; + o[ 7] = x5 >>> 24 & 0xff; + + o[ 8] = x10 >>> 0 & 0xff; + o[ 9] = x10 >>> 8 & 0xff; + o[10] = x10 >>> 16 & 0xff; + o[11] = x10 >>> 24 & 0xff; + + o[12] = x15 >>> 0 & 0xff; + o[13] = x15 >>> 8 & 0xff; + o[14] = x15 >>> 16 & 0xff; + o[15] = x15 >>> 24 & 0xff; + + o[16] = x6 >>> 0 & 0xff; + o[17] = x6 >>> 8 & 0xff; + o[18] = x6 >>> 16 & 0xff; + o[19] = x6 >>> 24 & 0xff; + + o[20] = x7 >>> 0 & 0xff; + o[21] = x7 >>> 8 & 0xff; + o[22] = x7 >>> 16 & 0xff; + o[23] = x7 >>> 24 & 0xff; + + o[24] = x8 >>> 0 & 0xff; + o[25] = x8 >>> 8 & 0xff; + o[26] = x8 >>> 16 & 0xff; + o[27] = x8 >>> 24 & 0xff; + + o[28] = x9 >>> 0 & 0xff; + o[29] = x9 >>> 8 & 0xff; + o[30] = x9 >>> 16 & 0xff; + o[31] = x9 >>> 24 & 0xff; +} + +function crypto_core_salsa20(out,inp,k,c) { + core_salsa20(out,inp,k,c); +} + +function crypto_core_hsalsa20(out,inp,k,c) { + core_hsalsa20(out,inp,k,c); +} + +var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]); + // "expand 32-byte k" + +function crypto_stream_salsa20_xor(c,cpos,m,mpos,b,n,k) { + var z = new Uint8Array(16), x = new Uint8Array(64); + var u, i; + for (i = 0; i < 16; i++) z[i] = 0; + for (i = 0; i < 8; i++) z[i] = n[i]; + while (b >= 64) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < 64; i++) c[cpos+i] = m[mpos+i] ^ x[i]; + u = 1; + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0; + z[i] = u & 0xff; + u >>>= 8; + } + b -= 64; + cpos += 64; + mpos += 64; + } + if (b > 0) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < b; i++) c[cpos+i] = m[mpos+i] ^ x[i]; + } + return 0; +} + +function crypto_stream_salsa20(c,cpos,b,n,k) { + var z = new Uint8Array(16), x = new Uint8Array(64); + var u, i; + for (i = 0; i < 16; i++) z[i] = 0; + for (i = 0; i < 8; i++) z[i] = n[i]; + while (b >= 64) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < 64; i++) c[cpos+i] = x[i]; + u = 1; + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0; + z[i] = u & 0xff; + u >>>= 8; + } + b -= 64; + cpos += 64; + } + if (b > 0) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < b; i++) c[cpos+i] = x[i]; + } + return 0; +} + +function crypto_stream(c,cpos,d,n,k) { + var s = new Uint8Array(32); + crypto_core_hsalsa20(s,n,k,sigma); + var sn = new Uint8Array(8); + for (var i = 0; i < 8; i++) sn[i] = n[i+16]; + return crypto_stream_salsa20(c,cpos,d,sn,s); +} + +function crypto_stream_xor(c,cpos,m,mpos,d,n,k) { + var s = new Uint8Array(32); + crypto_core_hsalsa20(s,n,k,sigma); + var sn = new Uint8Array(8); + for (var i = 0; i < 8; i++) sn[i] = n[i+16]; + return crypto_stream_salsa20_xor(c,cpos,m,mpos,d,sn,s); +} + +/* +* Port of Andrew Moon's Poly1305-donna-16. Public domain. +* https://github.com/floodyberry/poly1305-donna +*/ + +var poly1305 = function(key) { + this.buffer = new Uint8Array(16); + this.r = new Uint16Array(10); + this.h = new Uint16Array(10); + this.pad = new Uint16Array(8); + this.leftover = 0; + this.fin = 0; + + var t0, t1, t2, t3, t4, t5, t6, t7; + + t0 = key[ 0] & 0xff | (key[ 1] & 0xff) << 8; this.r[0] = ( t0 ) & 0x1fff; + t1 = key[ 2] & 0xff | (key[ 3] & 0xff) << 8; this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff; + t2 = key[ 4] & 0xff | (key[ 5] & 0xff) << 8; this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03; + t3 = key[ 6] & 0xff | (key[ 7] & 0xff) << 8; this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff; + t4 = key[ 8] & 0xff | (key[ 9] & 0xff) << 8; this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff; + this.r[5] = ((t4 >>> 1)) & 0x1ffe; + t5 = key[10] & 0xff | (key[11] & 0xff) << 8; this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff; + t6 = key[12] & 0xff | (key[13] & 0xff) << 8; this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81; + t7 = key[14] & 0xff | (key[15] & 0xff) << 8; this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff; + this.r[9] = ((t7 >>> 5)) & 0x007f; + + this.pad[0] = key[16] & 0xff | (key[17] & 0xff) << 8; + this.pad[1] = key[18] & 0xff | (key[19] & 0xff) << 8; + this.pad[2] = key[20] & 0xff | (key[21] & 0xff) << 8; + this.pad[3] = key[22] & 0xff | (key[23] & 0xff) << 8; + this.pad[4] = key[24] & 0xff | (key[25] & 0xff) << 8; + this.pad[5] = key[26] & 0xff | (key[27] & 0xff) << 8; + this.pad[6] = key[28] & 0xff | (key[29] & 0xff) << 8; + this.pad[7] = key[30] & 0xff | (key[31] & 0xff) << 8; +}; + +poly1305.prototype.blocks = function(m, mpos, bytes) { + var hibit = this.fin ? 0 : (1 << 11); + var t0, t1, t2, t3, t4, t5, t6, t7, c; + var d0, d1, d2, d3, d4, d5, d6, d7, d8, d9; + + var h0 = this.h[0], + h1 = this.h[1], + h2 = this.h[2], + h3 = this.h[3], + h4 = this.h[4], + h5 = this.h[5], + h6 = this.h[6], + h7 = this.h[7], + h8 = this.h[8], + h9 = this.h[9]; + + var r0 = this.r[0], + r1 = this.r[1], + r2 = this.r[2], + r3 = this.r[3], + r4 = this.r[4], + r5 = this.r[5], + r6 = this.r[6], + r7 = this.r[7], + r8 = this.r[8], + r9 = this.r[9]; + + while (bytes >= 16) { + t0 = m[mpos+ 0] & 0xff | (m[mpos+ 1] & 0xff) << 8; h0 += ( t0 ) & 0x1fff; + t1 = m[mpos+ 2] & 0xff | (m[mpos+ 3] & 0xff) << 8; h1 += ((t0 >>> 13) | (t1 << 3)) & 0x1fff; + t2 = m[mpos+ 4] & 0xff | (m[mpos+ 5] & 0xff) << 8; h2 += ((t1 >>> 10) | (t2 << 6)) & 0x1fff; + t3 = m[mpos+ 6] & 0xff | (m[mpos+ 7] & 0xff) << 8; h3 += ((t2 >>> 7) | (t3 << 9)) & 0x1fff; + t4 = m[mpos+ 8] & 0xff | (m[mpos+ 9] & 0xff) << 8; h4 += ((t3 >>> 4) | (t4 << 12)) & 0x1fff; + h5 += ((t4 >>> 1)) & 0x1fff; + t5 = m[mpos+10] & 0xff | (m[mpos+11] & 0xff) << 8; h6 += ((t4 >>> 14) | (t5 << 2)) & 0x1fff; + t6 = m[mpos+12] & 0xff | (m[mpos+13] & 0xff) << 8; h7 += ((t5 >>> 11) | (t6 << 5)) & 0x1fff; + t7 = m[mpos+14] & 0xff | (m[mpos+15] & 0xff) << 8; h8 += ((t6 >>> 8) | (t7 << 8)) & 0x1fff; + h9 += ((t7 >>> 5)) | hibit; + + c = 0; + + d0 = c; + d0 += h0 * r0; + d0 += h1 * (5 * r9); + d0 += h2 * (5 * r8); + d0 += h3 * (5 * r7); + d0 += h4 * (5 * r6); + c = (d0 >>> 13); d0 &= 0x1fff; + d0 += h5 * (5 * r5); + d0 += h6 * (5 * r4); + d0 += h7 * (5 * r3); + d0 += h8 * (5 * r2); + d0 += h9 * (5 * r1); + c += (d0 >>> 13); d0 &= 0x1fff; + + d1 = c; + d1 += h0 * r1; + d1 += h1 * r0; + d1 += h2 * (5 * r9); + d1 += h3 * (5 * r8); + d1 += h4 * (5 * r7); + c = (d1 >>> 13); d1 &= 0x1fff; + d1 += h5 * (5 * r6); + d1 += h6 * (5 * r5); + d1 += h7 * (5 * r4); + d1 += h8 * (5 * r3); + d1 += h9 * (5 * r2); + c += (d1 >>> 13); d1 &= 0x1fff; + + d2 = c; + d2 += h0 * r2; + d2 += h1 * r1; + d2 += h2 * r0; + d2 += h3 * (5 * r9); + d2 += h4 * (5 * r8); + c = (d2 >>> 13); d2 &= 0x1fff; + d2 += h5 * (5 * r7); + d2 += h6 * (5 * r6); + d2 += h7 * (5 * r5); + d2 += h8 * (5 * r4); + d2 += h9 * (5 * r3); + c += (d2 >>> 13); d2 &= 0x1fff; + + d3 = c; + d3 += h0 * r3; + d3 += h1 * r2; + d3 += h2 * r1; + d3 += h3 * r0; + d3 += h4 * (5 * r9); + c = (d3 >>> 13); d3 &= 0x1fff; + d3 += h5 * (5 * r8); + d3 += h6 * (5 * r7); + d3 += h7 * (5 * r6); + d3 += h8 * (5 * r5); + d3 += h9 * (5 * r4); + c += (d3 >>> 13); d3 &= 0x1fff; + + d4 = c; + d4 += h0 * r4; + d4 += h1 * r3; + d4 += h2 * r2; + d4 += h3 * r1; + d4 += h4 * r0; + c = (d4 >>> 13); d4 &= 0x1fff; + d4 += h5 * (5 * r9); + d4 += h6 * (5 * r8); + d4 += h7 * (5 * r7); + d4 += h8 * (5 * r6); + d4 += h9 * (5 * r5); + c += (d4 >>> 13); d4 &= 0x1fff; + + d5 = c; + d5 += h0 * r5; + d5 += h1 * r4; + d5 += h2 * r3; + d5 += h3 * r2; + d5 += h4 * r1; + c = (d5 >>> 13); d5 &= 0x1fff; + d5 += h5 * r0; + d5 += h6 * (5 * r9); + d5 += h7 * (5 * r8); + d5 += h8 * (5 * r7); + d5 += h9 * (5 * r6); + c += (d5 >>> 13); d5 &= 0x1fff; + + d6 = c; + d6 += h0 * r6; + d6 += h1 * r5; + d6 += h2 * r4; + d6 += h3 * r3; + d6 += h4 * r2; + c = (d6 >>> 13); d6 &= 0x1fff; + d6 += h5 * r1; + d6 += h6 * r0; + d6 += h7 * (5 * r9); + d6 += h8 * (5 * r8); + d6 += h9 * (5 * r7); + c += (d6 >>> 13); d6 &= 0x1fff; + + d7 = c; + d7 += h0 * r7; + d7 += h1 * r6; + d7 += h2 * r5; + d7 += h3 * r4; + d7 += h4 * r3; + c = (d7 >>> 13); d7 &= 0x1fff; + d7 += h5 * r2; + d7 += h6 * r1; + d7 += h7 * r0; + d7 += h8 * (5 * r9); + d7 += h9 * (5 * r8); + c += (d7 >>> 13); d7 &= 0x1fff; + + d8 = c; + d8 += h0 * r8; + d8 += h1 * r7; + d8 += h2 * r6; + d8 += h3 * r5; + d8 += h4 * r4; + c = (d8 >>> 13); d8 &= 0x1fff; + d8 += h5 * r3; + d8 += h6 * r2; + d8 += h7 * r1; + d8 += h8 * r0; + d8 += h9 * (5 * r9); + c += (d8 >>> 13); d8 &= 0x1fff; + + d9 = c; + d9 += h0 * r9; + d9 += h1 * r8; + d9 += h2 * r7; + d9 += h3 * r6; + d9 += h4 * r5; + c = (d9 >>> 13); d9 &= 0x1fff; + d9 += h5 * r4; + d9 += h6 * r3; + d9 += h7 * r2; + d9 += h8 * r1; + d9 += h9 * r0; + c += (d9 >>> 13); d9 &= 0x1fff; + + c = (((c << 2) + c)) | 0; + c = (c + d0) | 0; + d0 = c & 0x1fff; + c = (c >>> 13); + d1 += c; + + h0 = d0; + h1 = d1; + h2 = d2; + h3 = d3; + h4 = d4; + h5 = d5; + h6 = d6; + h7 = d7; + h8 = d8; + h9 = d9; + + mpos += 16; + bytes -= 16; + } + this.h[0] = h0; + this.h[1] = h1; + this.h[2] = h2; + this.h[3] = h3; + this.h[4] = h4; + this.h[5] = h5; + this.h[6] = h6; + this.h[7] = h7; + this.h[8] = h8; + this.h[9] = h9; +}; + +poly1305.prototype.finish = function(mac, macpos) { + var g = new Uint16Array(10); + var c, mask, f, i; + + if (this.leftover) { + i = this.leftover; + this.buffer[i++] = 1; + for (; i < 16; i++) this.buffer[i] = 0; + this.fin = 1; + this.blocks(this.buffer, 0, 16); + } + + c = this.h[1] >>> 13; + this.h[1] &= 0x1fff; + for (i = 2; i < 10; i++) { + this.h[i] += c; + c = this.h[i] >>> 13; + this.h[i] &= 0x1fff; + } + this.h[0] += (c * 5); + c = this.h[0] >>> 13; + this.h[0] &= 0x1fff; + this.h[1] += c; + c = this.h[1] >>> 13; + this.h[1] &= 0x1fff; + this.h[2] += c; + + g[0] = this.h[0] + 5; + c = g[0] >>> 13; + g[0] &= 0x1fff; + for (i = 1; i < 10; i++) { + g[i] = this.h[i] + c; + c = g[i] >>> 13; + g[i] &= 0x1fff; + } + g[9] -= (1 << 13); + + mask = (c ^ 1) - 1; + for (i = 0; i < 10; i++) g[i] &= mask; + mask = ~mask; + for (i = 0; i < 10; i++) this.h[i] = (this.h[i] & mask) | g[i]; + + this.h[0] = ((this.h[0] ) | (this.h[1] << 13) ) & 0xffff; + this.h[1] = ((this.h[1] >>> 3) | (this.h[2] << 10) ) & 0xffff; + this.h[2] = ((this.h[2] >>> 6) | (this.h[3] << 7) ) & 0xffff; + this.h[3] = ((this.h[3] >>> 9) | (this.h[4] << 4) ) & 0xffff; + this.h[4] = ((this.h[4] >>> 12) | (this.h[5] << 1) | (this.h[6] << 14)) & 0xffff; + this.h[5] = ((this.h[6] >>> 2) | (this.h[7] << 11) ) & 0xffff; + this.h[6] = ((this.h[7] >>> 5) | (this.h[8] << 8) ) & 0xffff; + this.h[7] = ((this.h[8] >>> 8) | (this.h[9] << 5) ) & 0xffff; + + f = this.h[0] + this.pad[0]; + this.h[0] = f & 0xffff; + for (i = 1; i < 8; i++) { + f = (((this.h[i] + this.pad[i]) | 0) + (f >>> 16)) | 0; + this.h[i] = f & 0xffff; + } + + mac[macpos+ 0] = (this.h[0] >>> 0) & 0xff; + mac[macpos+ 1] = (this.h[0] >>> 8) & 0xff; + mac[macpos+ 2] = (this.h[1] >>> 0) & 0xff; + mac[macpos+ 3] = (this.h[1] >>> 8) & 0xff; + mac[macpos+ 4] = (this.h[2] >>> 0) & 0xff; + mac[macpos+ 5] = (this.h[2] >>> 8) & 0xff; + mac[macpos+ 6] = (this.h[3] >>> 0) & 0xff; + mac[macpos+ 7] = (this.h[3] >>> 8) & 0xff; + mac[macpos+ 8] = (this.h[4] >>> 0) & 0xff; + mac[macpos+ 9] = (this.h[4] >>> 8) & 0xff; + mac[macpos+10] = (this.h[5] >>> 0) & 0xff; + mac[macpos+11] = (this.h[5] >>> 8) & 0xff; + mac[macpos+12] = (this.h[6] >>> 0) & 0xff; + mac[macpos+13] = (this.h[6] >>> 8) & 0xff; + mac[macpos+14] = (this.h[7] >>> 0) & 0xff; + mac[macpos+15] = (this.h[7] >>> 8) & 0xff; +}; + +poly1305.prototype.update = function(m, mpos, bytes) { + var i, want; + + if (this.leftover) { + want = (16 - this.leftover); + if (want > bytes) + want = bytes; + for (i = 0; i < want; i++) + this.buffer[this.leftover + i] = m[mpos+i]; + bytes -= want; + mpos += want; + this.leftover += want; + if (this.leftover < 16) + return; + this.blocks(this.buffer, 0, 16); + this.leftover = 0; + } + + if (bytes >= 16) { + want = bytes - (bytes % 16); + this.blocks(m, mpos, want); + mpos += want; + bytes -= want; + } + + if (bytes) { + for (i = 0; i < bytes; i++) + this.buffer[this.leftover + i] = m[mpos+i]; + this.leftover += bytes; + } +}; + +function crypto_onetimeauth(out, outpos, m, mpos, n, k) { + var s = new poly1305(k); + s.update(m, mpos, n); + s.finish(out, outpos); + return 0; +} + +function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) { + var x = new Uint8Array(16); + crypto_onetimeauth(x,0,m,mpos,n,k); + return crypto_verify_16(h,hpos,x,0); +} + +function crypto_secretbox(c,m,d,n,k) { + var i; + if (d < 32) return -1; + crypto_stream_xor(c,0,m,0,d,n,k); + crypto_onetimeauth(c, 16, c, 32, d - 32, c); + for (i = 0; i < 16; i++) c[i] = 0; + return 0; +} + +function crypto_secretbox_open(m,c,d,n,k) { + var i; + var x = new Uint8Array(32); + if (d < 32) return -1; + crypto_stream(x,0,32,n,k); + if (crypto_onetimeauth_verify(c, 16,c, 32,d - 32,x) !== 0) return -1; + crypto_stream_xor(m,0,c,0,d,n,k); + for (i = 0; i < 32; i++) m[i] = 0; + return 0; +} + +function set25519(r, a) { + var i; + for (i = 0; i < 16; i++) r[i] = a[i]|0; +} + +function car25519(o) { + var i, v, c = 1; + for (i = 0; i < 16; i++) { + v = o[i] + c + 65535; + c = Math.floor(v / 65536); + o[i] = v - c * 65536; + } + o[0] += c-1 + 37 * (c-1); +} + +function sel25519(p, q, b) { + var t, c = ~(b-1); + for (var i = 0; i < 16; i++) { + t = c & (p[i] ^ q[i]); + p[i] ^= t; + q[i] ^= t; + } +} + +function pack25519(o, n) { + var i, j, b; + var m = gf(), t = gf(); + for (i = 0; i < 16; i++) t[i] = n[i]; + car25519(t); + car25519(t); + car25519(t); + for (j = 0; j < 2; j++) { + m[0] = t[0] - 0xffed; + for (i = 1; i < 15; i++) { + m[i] = t[i] - 0xffff - ((m[i-1]>>16) & 1); + m[i-1] &= 0xffff; + } + m[15] = t[15] - 0x7fff - ((m[14]>>16) & 1); + b = (m[15]>>16) & 1; + m[14] &= 0xffff; + sel25519(t, m, 1-b); + } + for (i = 0; i < 16; i++) { + o[2*i] = t[i] & 0xff; + o[2*i+1] = t[i]>>8; + } +} + +function neq25519(a, b) { + var c = new Uint8Array(32), d = new Uint8Array(32); + pack25519(c, a); + pack25519(d, b); + return crypto_verify_32(c, 0, d, 0); +} + +function par25519(a) { + var d = new Uint8Array(32); + pack25519(d, a); + return d[0] & 1; +} + +function unpack25519(o, n) { + var i; + for (i = 0; i < 16; i++) o[i] = n[2*i] + (n[2*i+1] << 8); + o[15] &= 0x7fff; +} + +function A(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] + b[i]; +} + +function Z(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] - b[i]; +} + +function M(o, a, b) { + var v, c, + t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, + t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0, + t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0, + t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0, + b0 = b[0], + b1 = b[1], + b2 = b[2], + b3 = b[3], + b4 = b[4], + b5 = b[5], + b6 = b[6], + b7 = b[7], + b8 = b[8], + b9 = b[9], + b10 = b[10], + b11 = b[11], + b12 = b[12], + b13 = b[13], + b14 = b[14], + b15 = b[15]; + + v = a[0]; + t0 += v * b0; + t1 += v * b1; + t2 += v * b2; + t3 += v * b3; + t4 += v * b4; + t5 += v * b5; + t6 += v * b6; + t7 += v * b7; + t8 += v * b8; + t9 += v * b9; + t10 += v * b10; + t11 += v * b11; + t12 += v * b12; + t13 += v * b13; + t14 += v * b14; + t15 += v * b15; + v = a[1]; + t1 += v * b0; + t2 += v * b1; + t3 += v * b2; + t4 += v * b3; + t5 += v * b4; + t6 += v * b5; + t7 += v * b6; + t8 += v * b7; + t9 += v * b8; + t10 += v * b9; + t11 += v * b10; + t12 += v * b11; + t13 += v * b12; + t14 += v * b13; + t15 += v * b14; + t16 += v * b15; + v = a[2]; + t2 += v * b0; + t3 += v * b1; + t4 += v * b2; + t5 += v * b3; + t6 += v * b4; + t7 += v * b5; + t8 += v * b6; + t9 += v * b7; + t10 += v * b8; + t11 += v * b9; + t12 += v * b10; + t13 += v * b11; + t14 += v * b12; + t15 += v * b13; + t16 += v * b14; + t17 += v * b15; + v = a[3]; + t3 += v * b0; + t4 += v * b1; + t5 += v * b2; + t6 += v * b3; + t7 += v * b4; + t8 += v * b5; + t9 += v * b6; + t10 += v * b7; + t11 += v * b8; + t12 += v * b9; + t13 += v * b10; + t14 += v * b11; + t15 += v * b12; + t16 += v * b13; + t17 += v * b14; + t18 += v * b15; + v = a[4]; + t4 += v * b0; + t5 += v * b1; + t6 += v * b2; + t7 += v * b3; + t8 += v * b4; + t9 += v * b5; + t10 += v * b6; + t11 += v * b7; + t12 += v * b8; + t13 += v * b9; + t14 += v * b10; + t15 += v * b11; + t16 += v * b12; + t17 += v * b13; + t18 += v * b14; + t19 += v * b15; + v = a[5]; + t5 += v * b0; + t6 += v * b1; + t7 += v * b2; + t8 += v * b3; + t9 += v * b4; + t10 += v * b5; + t11 += v * b6; + t12 += v * b7; + t13 += v * b8; + t14 += v * b9; + t15 += v * b10; + t16 += v * b11; + t17 += v * b12; + t18 += v * b13; + t19 += v * b14; + t20 += v * b15; + v = a[6]; + t6 += v * b0; + t7 += v * b1; + t8 += v * b2; + t9 += v * b3; + t10 += v * b4; + t11 += v * b5; + t12 += v * b6; + t13 += v * b7; + t14 += v * b8; + t15 += v * b9; + t16 += v * b10; + t17 += v * b11; + t18 += v * b12; + t19 += v * b13; + t20 += v * b14; + t21 += v * b15; + v = a[7]; + t7 += v * b0; + t8 += v * b1; + t9 += v * b2; + t10 += v * b3; + t11 += v * b4; + t12 += v * b5; + t13 += v * b6; + t14 += v * b7; + t15 += v * b8; + t16 += v * b9; + t17 += v * b10; + t18 += v * b11; + t19 += v * b12; + t20 += v * b13; + t21 += v * b14; + t22 += v * b15; + v = a[8]; + t8 += v * b0; + t9 += v * b1; + t10 += v * b2; + t11 += v * b3; + t12 += v * b4; + t13 += v * b5; + t14 += v * b6; + t15 += v * b7; + t16 += v * b8; + t17 += v * b9; + t18 += v * b10; + t19 += v * b11; + t20 += v * b12; + t21 += v * b13; + t22 += v * b14; + t23 += v * b15; + v = a[9]; + t9 += v * b0; + t10 += v * b1; + t11 += v * b2; + t12 += v * b3; + t13 += v * b4; + t14 += v * b5; + t15 += v * b6; + t16 += v * b7; + t17 += v * b8; + t18 += v * b9; + t19 += v * b10; + t20 += v * b11; + t21 += v * b12; + t22 += v * b13; + t23 += v * b14; + t24 += v * b15; + v = a[10]; + t10 += v * b0; + t11 += v * b1; + t12 += v * b2; + t13 += v * b3; + t14 += v * b4; + t15 += v * b5; + t16 += v * b6; + t17 += v * b7; + t18 += v * b8; + t19 += v * b9; + t20 += v * b10; + t21 += v * b11; + t22 += v * b12; + t23 += v * b13; + t24 += v * b14; + t25 += v * b15; + v = a[11]; + t11 += v * b0; + t12 += v * b1; + t13 += v * b2; + t14 += v * b3; + t15 += v * b4; + t16 += v * b5; + t17 += v * b6; + t18 += v * b7; + t19 += v * b8; + t20 += v * b9; + t21 += v * b10; + t22 += v * b11; + t23 += v * b12; + t24 += v * b13; + t25 += v * b14; + t26 += v * b15; + v = a[12]; + t12 += v * b0; + t13 += v * b1; + t14 += v * b2; + t15 += v * b3; + t16 += v * b4; + t17 += v * b5; + t18 += v * b6; + t19 += v * b7; + t20 += v * b8; + t21 += v * b9; + t22 += v * b10; + t23 += v * b11; + t24 += v * b12; + t25 += v * b13; + t26 += v * b14; + t27 += v * b15; + v = a[13]; + t13 += v * b0; + t14 += v * b1; + t15 += v * b2; + t16 += v * b3; + t17 += v * b4; + t18 += v * b5; + t19 += v * b6; + t20 += v * b7; + t21 += v * b8; + t22 += v * b9; + t23 += v * b10; + t24 += v * b11; + t25 += v * b12; + t26 += v * b13; + t27 += v * b14; + t28 += v * b15; + v = a[14]; + t14 += v * b0; + t15 += v * b1; + t16 += v * b2; + t17 += v * b3; + t18 += v * b4; + t19 += v * b5; + t20 += v * b6; + t21 += v * b7; + t22 += v * b8; + t23 += v * b9; + t24 += v * b10; + t25 += v * b11; + t26 += v * b12; + t27 += v * b13; + t28 += v * b14; + t29 += v * b15; + v = a[15]; + t15 += v * b0; + t16 += v * b1; + t17 += v * b2; + t18 += v * b3; + t19 += v * b4; + t20 += v * b5; + t21 += v * b6; + t22 += v * b7; + t23 += v * b8; + t24 += v * b9; + t25 += v * b10; + t26 += v * b11; + t27 += v * b12; + t28 += v * b13; + t29 += v * b14; + t30 += v * b15; + + t0 += 38 * t16; + t1 += 38 * t17; + t2 += 38 * t18; + t3 += 38 * t19; + t4 += 38 * t20; + t5 += 38 * t21; + t6 += 38 * t22; + t7 += 38 * t23; + t8 += 38 * t24; + t9 += 38 * t25; + t10 += 38 * t26; + t11 += 38 * t27; + t12 += 38 * t28; + t13 += 38 * t29; + t14 += 38 * t30; + // t15 left as is + + // first car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + // second car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + o[ 0] = t0; + o[ 1] = t1; + o[ 2] = t2; + o[ 3] = t3; + o[ 4] = t4; + o[ 5] = t5; + o[ 6] = t6; + o[ 7] = t7; + o[ 8] = t8; + o[ 9] = t9; + o[10] = t10; + o[11] = t11; + o[12] = t12; + o[13] = t13; + o[14] = t14; + o[15] = t15; +} + +function S(o, a) { + M(o, a, a); +} + +function inv25519(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 253; a >= 0; a--) { + S(c, c); + if(a !== 2 && a !== 4) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; +} + +function pow2523(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 250; a >= 0; a--) { + S(c, c); + if(a !== 1) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; +} + +function crypto_scalarmult(q, n, p) { + var z = new Uint8Array(32); + var x = new Float64Array(80), r, i; + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(); + for (i = 0; i < 31; i++) z[i] = n[i]; + z[31]=(n[31]&127)|64; + z[0]&=248; + unpack25519(x,p); + for (i = 0; i < 16; i++) { + b[i]=x[i]; + d[i]=a[i]=c[i]=0; + } + a[0]=d[0]=1; + for (i=254; i>=0; --i) { + r=(z[i>>>3]>>>(i&7))&1; + sel25519(a,b,r); + sel25519(c,d,r); + A(e,a,c); + Z(a,a,c); + A(c,b,d); + Z(b,b,d); + S(d,e); + S(f,a); + M(a,c,a); + M(c,b,e); + A(e,a,c); + Z(a,a,c); + S(b,a); + Z(c,d,f); + M(a,c,_121665); + A(a,a,d); + M(c,c,a); + M(a,d,f); + M(d,b,x); + S(b,e); + sel25519(a,b,r); + sel25519(c,d,r); + } + for (i = 0; i < 16; i++) { + x[i+16]=a[i]; + x[i+32]=c[i]; + x[i+48]=b[i]; + x[i+64]=d[i]; + } + var x32 = x.subarray(32); + var x16 = x.subarray(16); + inv25519(x32,x32); + M(x16,x16,x32); + pack25519(q,x16); + return 0; +} + +function crypto_scalarmult_base(q, n) { + return crypto_scalarmult(q, n, _9); +} + +function crypto_box_keypair(y, x) { + randombytes(x, 32); + return crypto_scalarmult_base(y, x); +} + +function crypto_box_beforenm(k, y, x) { + var s = new Uint8Array(32); + crypto_scalarmult(s, x, y); + return crypto_core_hsalsa20(k, _0, s, sigma); +} + +var crypto_box_afternm = crypto_secretbox; +var crypto_box_open_afternm = crypto_secretbox_open; + +function crypto_box(c, m, d, n, y, x) { + var k = new Uint8Array(32); + crypto_box_beforenm(k, y, x); + return crypto_box_afternm(c, m, d, n, k); +} + +function crypto_box_open(m, c, d, n, y, x) { + var k = new Uint8Array(32); + crypto_box_beforenm(k, y, x); + return crypto_box_open_afternm(m, c, d, n, k); +} + +var K = [ + 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, + 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc, + 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, + 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, + 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe, + 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2, + 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, + 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694, + 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, + 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, + 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483, + 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5, + 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, + 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4, + 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, + 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, + 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926, + 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df, + 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, + 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b, + 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, + 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, + 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910, + 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8, + 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, + 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8, + 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, + 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, + 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60, + 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec, + 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, + 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b, + 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, + 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, + 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6, + 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b, + 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, + 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c, + 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, + 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817 +]; + +function crypto_hashblocks_hl(hh, hl, m, n) { + var wh = new Int32Array(16), wl = new Int32Array(16), + bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7, + bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7, + th, tl, i, j, h, l, a, b, c, d; + + var ah0 = hh[0], + ah1 = hh[1], + ah2 = hh[2], + ah3 = hh[3], + ah4 = hh[4], + ah5 = hh[5], + ah6 = hh[6], + ah7 = hh[7], + + al0 = hl[0], + al1 = hl[1], + al2 = hl[2], + al3 = hl[3], + al4 = hl[4], + al5 = hl[5], + al6 = hl[6], + al7 = hl[7]; + + var pos = 0; + while (n >= 128) { + for (i = 0; i < 16; i++) { + j = 8 * i + pos; + wh[i] = (m[j+0] << 24) | (m[j+1] << 16) | (m[j+2] << 8) | m[j+3]; + wl[i] = (m[j+4] << 24) | (m[j+5] << 16) | (m[j+6] << 8) | m[j+7]; + } + for (i = 0; i < 80; i++) { + bh0 = ah0; + bh1 = ah1; + bh2 = ah2; + bh3 = ah3; + bh4 = ah4; + bh5 = ah5; + bh6 = ah6; + bh7 = ah7; + + bl0 = al0; + bl1 = al1; + bl2 = al2; + bl3 = al3; + bl4 = al4; + bl5 = al5; + bl6 = al6; + bl7 = al7; + + // add + h = ah7; + l = al7; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + // Sigma1 + h = ((ah4 >>> 14) | (al4 << (32-14))) ^ ((ah4 >>> 18) | (al4 << (32-18))) ^ ((al4 >>> (41-32)) | (ah4 << (32-(41-32)))); + l = ((al4 >>> 14) | (ah4 << (32-14))) ^ ((al4 >>> 18) | (ah4 << (32-18))) ^ ((ah4 >>> (41-32)) | (al4 << (32-(41-32)))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // Ch + h = (ah4 & ah5) ^ (~ah4 & ah6); + l = (al4 & al5) ^ (~al4 & al6); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // K + h = K[i*2]; + l = K[i*2+1]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // w + h = wh[i%16]; + l = wl[i%16]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + th = c & 0xffff | d << 16; + tl = a & 0xffff | b << 16; + + // add + h = th; + l = tl; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + // Sigma0 + h = ((ah0 >>> 28) | (al0 << (32-28))) ^ ((al0 >>> (34-32)) | (ah0 << (32-(34-32)))) ^ ((al0 >>> (39-32)) | (ah0 << (32-(39-32)))); + l = ((al0 >>> 28) | (ah0 << (32-28))) ^ ((ah0 >>> (34-32)) | (al0 << (32-(34-32)))) ^ ((ah0 >>> (39-32)) | (al0 << (32-(39-32)))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // Maj + h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2); + l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + bh7 = (c & 0xffff) | (d << 16); + bl7 = (a & 0xffff) | (b << 16); + + // add + h = bh3; + l = bl3; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = th; + l = tl; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + bh3 = (c & 0xffff) | (d << 16); + bl3 = (a & 0xffff) | (b << 16); + + ah1 = bh0; + ah2 = bh1; + ah3 = bh2; + ah4 = bh3; + ah5 = bh4; + ah6 = bh5; + ah7 = bh6; + ah0 = bh7; + + al1 = bl0; + al2 = bl1; + al3 = bl2; + al4 = bl3; + al5 = bl4; + al6 = bl5; + al7 = bl6; + al0 = bl7; + + if (i%16 === 15) { + for (j = 0; j < 16; j++) { + // add + h = wh[j]; + l = wl[j]; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = wh[(j+9)%16]; + l = wl[(j+9)%16]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // sigma0 + th = wh[(j+1)%16]; + tl = wl[(j+1)%16]; + h = ((th >>> 1) | (tl << (32-1))) ^ ((th >>> 8) | (tl << (32-8))) ^ (th >>> 7); + l = ((tl >>> 1) | (th << (32-1))) ^ ((tl >>> 8) | (th << (32-8))) ^ ((tl >>> 7) | (th << (32-7))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // sigma1 + th = wh[(j+14)%16]; + tl = wl[(j+14)%16]; + h = ((th >>> 19) | (tl << (32-19))) ^ ((tl >>> (61-32)) | (th << (32-(61-32)))) ^ (th >>> 6); + l = ((tl >>> 19) | (th << (32-19))) ^ ((th >>> (61-32)) | (tl << (32-(61-32)))) ^ ((tl >>> 6) | (th << (32-6))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + wh[j] = (c & 0xffff) | (d << 16); + wl[j] = (a & 0xffff) | (b << 16); + } + } + } + + // add + h = ah0; + l = al0; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[0]; + l = hl[0]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[0] = ah0 = (c & 0xffff) | (d << 16); + hl[0] = al0 = (a & 0xffff) | (b << 16); + + h = ah1; + l = al1; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[1]; + l = hl[1]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[1] = ah1 = (c & 0xffff) | (d << 16); + hl[1] = al1 = (a & 0xffff) | (b << 16); + + h = ah2; + l = al2; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[2]; + l = hl[2]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[2] = ah2 = (c & 0xffff) | (d << 16); + hl[2] = al2 = (a & 0xffff) | (b << 16); + + h = ah3; + l = al3; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[3]; + l = hl[3]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[3] = ah3 = (c & 0xffff) | (d << 16); + hl[3] = al3 = (a & 0xffff) | (b << 16); + + h = ah4; + l = al4; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[4]; + l = hl[4]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[4] = ah4 = (c & 0xffff) | (d << 16); + hl[4] = al4 = (a & 0xffff) | (b << 16); + + h = ah5; + l = al5; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[5]; + l = hl[5]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[5] = ah5 = (c & 0xffff) | (d << 16); + hl[5] = al5 = (a & 0xffff) | (b << 16); + + h = ah6; + l = al6; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[6]; + l = hl[6]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[6] = ah6 = (c & 0xffff) | (d << 16); + hl[6] = al6 = (a & 0xffff) | (b << 16); + + h = ah7; + l = al7; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[7]; + l = hl[7]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[7] = ah7 = (c & 0xffff) | (d << 16); + hl[7] = al7 = (a & 0xffff) | (b << 16); + + pos += 128; + n -= 128; + } + + return n; +} + +function crypto_hash(out, m, n) { + var hh = new Int32Array(8), + hl = new Int32Array(8), + x = new Uint8Array(256), + i, b = n; + + hh[0] = 0x6a09e667; + hh[1] = 0xbb67ae85; + hh[2] = 0x3c6ef372; + hh[3] = 0xa54ff53a; + hh[4] = 0x510e527f; + hh[5] = 0x9b05688c; + hh[6] = 0x1f83d9ab; + hh[7] = 0x5be0cd19; + + hl[0] = 0xf3bcc908; + hl[1] = 0x84caa73b; + hl[2] = 0xfe94f82b; + hl[3] = 0x5f1d36f1; + hl[4] = 0xade682d1; + hl[5] = 0x2b3e6c1f; + hl[6] = 0xfb41bd6b; + hl[7] = 0x137e2179; + + crypto_hashblocks_hl(hh, hl, m, n); + n %= 128; + + for (i = 0; i < n; i++) x[i] = m[b-n+i]; + x[n] = 128; + + n = 256-128*(n<112?1:0); + x[n-9] = 0; + ts64(x, n-8, (b / 0x20000000) | 0, b << 3); + crypto_hashblocks_hl(hh, hl, x, n); + + for (i = 0; i < 8; i++) ts64(out, 8*i, hh[i], hl[i]); + + return 0; +} + +function add(p, q) { + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(), + g = gf(), h = gf(), t = gf(); + + Z(a, p[1], p[0]); + Z(t, q[1], q[0]); + M(a, a, t); + A(b, p[0], p[1]); + A(t, q[0], q[1]); + M(b, b, t); + M(c, p[3], q[3]); + M(c, c, D2); + M(d, p[2], q[2]); + A(d, d, d); + Z(e, b, a); + Z(f, d, c); + A(g, d, c); + A(h, b, a); + + M(p[0], e, f); + M(p[1], h, g); + M(p[2], g, f); + M(p[3], e, h); +} + +function cswap(p, q, b) { + var i; + for (i = 0; i < 4; i++) { + sel25519(p[i], q[i], b); + } +} + +function pack(r, p) { + var tx = gf(), ty = gf(), zi = gf(); + inv25519(zi, p[2]); + M(tx, p[0], zi); + M(ty, p[1], zi); + pack25519(r, ty); + r[31] ^= par25519(tx) << 7; +} + +function scalarmult(p, q, s) { + var b, i; + set25519(p[0], gf0); + set25519(p[1], gf1); + set25519(p[2], gf1); + set25519(p[3], gf0); + for (i = 255; i >= 0; --i) { + b = (s[(i/8)|0] >> (i&7)) & 1; + cswap(p, q, b); + add(q, p); + add(p, p); + cswap(p, q, b); + } +} + +function scalarbase(p, s) { + var q = [gf(), gf(), gf(), gf()]; + set25519(q[0], X); + set25519(q[1], Y); + set25519(q[2], gf1); + M(q[3], X, Y); + scalarmult(p, q, s); +} + +function crypto_sign_keypair(pk, sk, seeded) { + var d = new Uint8Array(64); + var p = [gf(), gf(), gf(), gf()]; + var i; + + if (!seeded) randombytes(sk, 32); + crypto_hash(d, sk, 32); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + scalarbase(p, d); + pack(pk, p); + + for (i = 0; i < 32; i++) sk[i+32] = pk[i]; + return 0; +} + +var L = new Float64Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]); + +function modL(r, x) { + var carry, i, j, k; + for (i = 63; i >= 32; --i) { + carry = 0; + for (j = i - 32, k = i - 12; j < k; ++j) { + x[j] += carry - 16 * x[i] * L[j - (i - 32)]; + carry = Math.floor((x[j] + 128) / 256); + x[j] -= carry * 256; + } + x[j] += carry; + x[i] = 0; + } + carry = 0; + for (j = 0; j < 32; j++) { + x[j] += carry - (x[31] >> 4) * L[j]; + carry = x[j] >> 8; + x[j] &= 255; + } + for (j = 0; j < 32; j++) x[j] -= carry * L[j]; + for (i = 0; i < 32; i++) { + x[i+1] += x[i] >> 8; + r[i] = x[i] & 255; + } +} + +function reduce(r) { + var x = new Float64Array(64), i; + for (i = 0; i < 64; i++) x[i] = r[i]; + for (i = 0; i < 64; i++) r[i] = 0; + modL(r, x); +} + +// Note: difference from C - smlen returned, not passed as argument. +function crypto_sign(sm, m, n, sk) { + var d = new Uint8Array(64), h = new Uint8Array(64), r = new Uint8Array(64); + var i, j, x = new Float64Array(64); + var p = [gf(), gf(), gf(), gf()]; + + crypto_hash(d, sk, 32); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + var smlen = n + 64; + for (i = 0; i < n; i++) sm[64 + i] = m[i]; + for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i]; + + crypto_hash(r, sm.subarray(32), n+32); + reduce(r); + scalarbase(p, r); + pack(sm, p); + + for (i = 32; i < 64; i++) sm[i] = sk[i]; + crypto_hash(h, sm, n + 64); + reduce(h); + + for (i = 0; i < 64; i++) x[i] = 0; + for (i = 0; i < 32; i++) x[i] = r[i]; + for (i = 0; i < 32; i++) { + for (j = 0; j < 32; j++) { + x[i+j] += h[i] * d[j]; + } + } + + modL(sm.subarray(32), x); + return smlen; +} + +function unpackneg(r, p) { + var t = gf(), chk = gf(), num = gf(), + den = gf(), den2 = gf(), den4 = gf(), + den6 = gf(); + + set25519(r[2], gf1); + unpack25519(r[1], p); + S(num, r[1]); + M(den, num, D); + Z(num, num, r[2]); + A(den, r[2], den); + + S(den2, den); + S(den4, den2); + M(den6, den4, den2); + M(t, den6, num); + M(t, t, den); + + pow2523(t, t); + M(t, t, num); + M(t, t, den); + M(t, t, den); + M(r[0], t, den); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) M(r[0], r[0], I); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) return -1; + + if (par25519(r[0]) === (p[31]>>7)) Z(r[0], gf0, r[0]); + + M(r[3], r[0], r[1]); + return 0; +} + +function crypto_sign_open(m, sm, n, pk) { + var i; + var t = new Uint8Array(32), h = new Uint8Array(64); + var p = [gf(), gf(), gf(), gf()], + q = [gf(), gf(), gf(), gf()]; + + if (n < 64) return -1; + + if (unpackneg(q, pk)) return -1; + + for (i = 0; i < n; i++) m[i] = sm[i]; + for (i = 0; i < 32; i++) m[i+32] = pk[i]; + crypto_hash(h, m, n); + reduce(h); + scalarmult(p, q, h); + + scalarbase(q, sm.subarray(32)); + add(p, q); + pack(t, p); + + n -= 64; + if (crypto_verify_32(sm, 0, t, 0)) { + for (i = 0; i < n; i++) m[i] = 0; + return -1; + } + + for (i = 0; i < n; i++) m[i] = sm[i + 64]; + return n; +} + +var crypto_secretbox_KEYBYTES = 32, + crypto_secretbox_NONCEBYTES = 24, + crypto_secretbox_ZEROBYTES = 32, + crypto_secretbox_BOXZEROBYTES = 16, + crypto_scalarmult_BYTES = 32, + crypto_scalarmult_SCALARBYTES = 32, + crypto_box_PUBLICKEYBYTES = 32, + crypto_box_SECRETKEYBYTES = 32, + crypto_box_BEFORENMBYTES = 32, + crypto_box_NONCEBYTES = crypto_secretbox_NONCEBYTES, + crypto_box_ZEROBYTES = crypto_secretbox_ZEROBYTES, + crypto_box_BOXZEROBYTES = crypto_secretbox_BOXZEROBYTES, + crypto_sign_BYTES = 64, + crypto_sign_PUBLICKEYBYTES = 32, + crypto_sign_SECRETKEYBYTES = 64, + crypto_sign_SEEDBYTES = 32, + crypto_hash_BYTES = 64; + +nacl.lowlevel = { + crypto_core_hsalsa20: crypto_core_hsalsa20, + crypto_stream_xor: crypto_stream_xor, + crypto_stream: crypto_stream, + crypto_stream_salsa20_xor: crypto_stream_salsa20_xor, + crypto_stream_salsa20: crypto_stream_salsa20, + crypto_onetimeauth: crypto_onetimeauth, + crypto_onetimeauth_verify: crypto_onetimeauth_verify, + crypto_verify_16: crypto_verify_16, + crypto_verify_32: crypto_verify_32, + crypto_secretbox: crypto_secretbox, + crypto_secretbox_open: crypto_secretbox_open, + crypto_scalarmult: crypto_scalarmult, + crypto_scalarmult_base: crypto_scalarmult_base, + crypto_box_beforenm: crypto_box_beforenm, + crypto_box_afternm: crypto_box_afternm, + crypto_box: crypto_box, + crypto_box_open: crypto_box_open, + crypto_box_keypair: crypto_box_keypair, + crypto_hash: crypto_hash, + crypto_sign: crypto_sign, + crypto_sign_keypair: crypto_sign_keypair, + crypto_sign_open: crypto_sign_open, + + crypto_secretbox_KEYBYTES: crypto_secretbox_KEYBYTES, + crypto_secretbox_NONCEBYTES: crypto_secretbox_NONCEBYTES, + crypto_secretbox_ZEROBYTES: crypto_secretbox_ZEROBYTES, + crypto_secretbox_BOXZEROBYTES: crypto_secretbox_BOXZEROBYTES, + crypto_scalarmult_BYTES: crypto_scalarmult_BYTES, + crypto_scalarmult_SCALARBYTES: crypto_scalarmult_SCALARBYTES, + crypto_box_PUBLICKEYBYTES: crypto_box_PUBLICKEYBYTES, + crypto_box_SECRETKEYBYTES: crypto_box_SECRETKEYBYTES, + crypto_box_BEFORENMBYTES: crypto_box_BEFORENMBYTES, + crypto_box_NONCEBYTES: crypto_box_NONCEBYTES, + crypto_box_ZEROBYTES: crypto_box_ZEROBYTES, + crypto_box_BOXZEROBYTES: crypto_box_BOXZEROBYTES, + crypto_sign_BYTES: crypto_sign_BYTES, + crypto_sign_PUBLICKEYBYTES: crypto_sign_PUBLICKEYBYTES, + crypto_sign_SECRETKEYBYTES: crypto_sign_SECRETKEYBYTES, + crypto_sign_SEEDBYTES: crypto_sign_SEEDBYTES, + crypto_hash_BYTES: crypto_hash_BYTES, + + gf: gf, + D: D, + L: L, + pack25519: pack25519, + unpack25519: unpack25519, + M: M, + A: A, + S: S, + Z: Z, + pow2523: pow2523, + add: add, + set25519: set25519, + modL: modL, + scalarmult: scalarmult, + scalarbase: scalarbase, +}; + +/* High-level API */ + +function checkLengths(k, n) { + if (k.length !== crypto_secretbox_KEYBYTES) throw new Error('bad key size'); + if (n.length !== crypto_secretbox_NONCEBYTES) throw new Error('bad nonce size'); +} + +function checkBoxLengths(pk, sk) { + if (pk.length !== crypto_box_PUBLICKEYBYTES) throw new Error('bad public key size'); + if (sk.length !== crypto_box_SECRETKEYBYTES) throw new Error('bad secret key size'); +} + +function checkArrayTypes() { + for (var i = 0; i < arguments.length; i++) { + if (!(arguments[i] instanceof Uint8Array)) + throw new TypeError('unexpected type, use Uint8Array'); + } +} + +function cleanup(arr) { + for (var i = 0; i < arr.length; i++) arr[i] = 0; +} + +nacl.randomBytes = function(n) { + var b = new Uint8Array(n); + randombytes(b, n); + return b; +}; + +nacl.secretbox = function(msg, nonce, key) { + checkArrayTypes(msg, nonce, key); + checkLengths(key, nonce); + var m = new Uint8Array(crypto_secretbox_ZEROBYTES + msg.length); + var c = new Uint8Array(m.length); + for (var i = 0; i < msg.length; i++) m[i+crypto_secretbox_ZEROBYTES] = msg[i]; + crypto_secretbox(c, m, m.length, nonce, key); + return c.subarray(crypto_secretbox_BOXZEROBYTES); +}; + +nacl.secretbox.open = function(box, nonce, key) { + checkArrayTypes(box, nonce, key); + checkLengths(key, nonce); + var c = new Uint8Array(crypto_secretbox_BOXZEROBYTES + box.length); + var m = new Uint8Array(c.length); + for (var i = 0; i < box.length; i++) c[i+crypto_secretbox_BOXZEROBYTES] = box[i]; + if (c.length < 32) return null; + if (crypto_secretbox_open(m, c, c.length, nonce, key) !== 0) return null; + return m.subarray(crypto_secretbox_ZEROBYTES); +}; + +nacl.secretbox.keyLength = crypto_secretbox_KEYBYTES; +nacl.secretbox.nonceLength = crypto_secretbox_NONCEBYTES; +nacl.secretbox.overheadLength = crypto_secretbox_BOXZEROBYTES; + +nacl.scalarMult = function(n, p) { + checkArrayTypes(n, p); + if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); + if (p.length !== crypto_scalarmult_BYTES) throw new Error('bad p size'); + var q = new Uint8Array(crypto_scalarmult_BYTES); + crypto_scalarmult(q, n, p); + return q; +}; + +nacl.scalarMult.base = function(n) { + checkArrayTypes(n); + if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); + var q = new Uint8Array(crypto_scalarmult_BYTES); + crypto_scalarmult_base(q, n); + return q; +}; + +nacl.scalarMult.scalarLength = crypto_scalarmult_SCALARBYTES; +nacl.scalarMult.groupElementLength = crypto_scalarmult_BYTES; + +nacl.box = function(msg, nonce, publicKey, secretKey) { + var k = nacl.box.before(publicKey, secretKey); + return nacl.secretbox(msg, nonce, k); +}; + +nacl.box.before = function(publicKey, secretKey) { + checkArrayTypes(publicKey, secretKey); + checkBoxLengths(publicKey, secretKey); + var k = new Uint8Array(crypto_box_BEFORENMBYTES); + crypto_box_beforenm(k, publicKey, secretKey); + return k; +}; + +nacl.box.after = nacl.secretbox; + +nacl.box.open = function(msg, nonce, publicKey, secretKey) { + var k = nacl.box.before(publicKey, secretKey); + return nacl.secretbox.open(msg, nonce, k); +}; + +nacl.box.open.after = nacl.secretbox.open; + +nacl.box.keyPair = function() { + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_box_SECRETKEYBYTES); + crypto_box_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.box.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_box_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + crypto_scalarmult_base(pk, secretKey); + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; +}; + +nacl.box.publicKeyLength = crypto_box_PUBLICKEYBYTES; +nacl.box.secretKeyLength = crypto_box_SECRETKEYBYTES; +nacl.box.sharedKeyLength = crypto_box_BEFORENMBYTES; +nacl.box.nonceLength = crypto_box_NONCEBYTES; +nacl.box.overheadLength = nacl.secretbox.overheadLength; + +nacl.sign = function(msg, secretKey) { + checkArrayTypes(msg, secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var signedMsg = new Uint8Array(crypto_sign_BYTES+msg.length); + crypto_sign(signedMsg, msg, msg.length, secretKey); + return signedMsg; +}; + +nacl.sign.open = function(signedMsg, publicKey) { + checkArrayTypes(signedMsg, publicKey); + if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) + throw new Error('bad public key size'); + var tmp = new Uint8Array(signedMsg.length); + var mlen = crypto_sign_open(tmp, signedMsg, signedMsg.length, publicKey); + if (mlen < 0) return null; + var m = new Uint8Array(mlen); + for (var i = 0; i < m.length; i++) m[i] = tmp[i]; + return m; +}; + +nacl.sign.detached = function(msg, secretKey) { + var signedMsg = nacl.sign(msg, secretKey); + var sig = new Uint8Array(crypto_sign_BYTES); + for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; + return sig; +}; + +nacl.sign.detached.verify = function(msg, sig, publicKey) { + checkArrayTypes(msg, sig, publicKey); + if (sig.length !== crypto_sign_BYTES) + throw new Error('bad signature size'); + if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) + throw new Error('bad public key size'); + var sm = new Uint8Array(crypto_sign_BYTES + msg.length); + var m = new Uint8Array(crypto_sign_BYTES + msg.length); + var i; + for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i]; + for (i = 0; i < msg.length; i++) sm[i+crypto_sign_BYTES] = msg[i]; + return (crypto_sign_open(m, sm, sm.length, publicKey) >= 0); +}; + +nacl.sign.keyPair = function() { + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + crypto_sign_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.sign.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + for (var i = 0; i < pk.length; i++) pk[i] = secretKey[32+i]; + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; +}; + +nacl.sign.keyPair.fromSeed = function(seed) { + checkArrayTypes(seed); + if (seed.length !== crypto_sign_SEEDBYTES) + throw new Error('bad seed size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + for (var i = 0; i < 32; i++) sk[i] = seed[i]; + crypto_sign_keypair(pk, sk, true); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.sign.publicKeyLength = crypto_sign_PUBLICKEYBYTES; +nacl.sign.secretKeyLength = crypto_sign_SECRETKEYBYTES; +nacl.sign.seedLength = crypto_sign_SEEDBYTES; +nacl.sign.signatureLength = crypto_sign_BYTES; + +nacl.hash = function(msg) { + checkArrayTypes(msg); + var h = new Uint8Array(crypto_hash_BYTES); + crypto_hash(h, msg, msg.length); + return h; +}; + +nacl.hash.hashLength = crypto_hash_BYTES; + +nacl.verify = function(x, y) { + checkArrayTypes(x, y); + // Zero length arguments are considered not equal. + if (x.length === 0 || y.length === 0) return false; + if (x.length !== y.length) return false; + return (vn(x, 0, y, 0, x.length) === 0) ? true : false; +}; + +nacl.setPRNG = function(fn) { + randombytes = fn; +}; + +(function() { + // Initialize PRNG if environment provides CSPRNG. + // If not, methods calling randombytes will throw. + var crypto = typeof self !== 'undefined' ? (self.crypto || self.msCrypto) : null; + if (crypto && crypto.getRandomValues) { + // Browsers. + var QUOTA = 65536; + nacl.setPRNG(function(x, n) { + var i, v = new Uint8Array(n); + for (i = 0; i < n; i += QUOTA) { + crypto.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA))); + } + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } else if (true) { + // Node.js. + crypto = __nccwpck_require__(6113); + if (crypto && crypto.randomBytes) { + nacl.setPRNG(function(x, n) { + var i, v = crypto.randomBytes(n); + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } + } +})(); + +})( true && module.exports ? module.exports : (self.nacl = self.nacl || {})); + + /***/ }), /***/ 25716: @@ -54656,6 +56661,371 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 56637: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SETUP_FEATURE_DESCRIPTIONS = exports.SETUP_AGENT_TASKS = void 0; +exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; +exports.mergeSetupConfiguration = mergeSetupConfiguration; +exports.validateSetupConfiguration = validateSetupConfiguration; +exports.buildSetupPlan = buildSetupPlan; +exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; +exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; +exports.buildSetupActionInputs = buildSetupActionInputs; +const agent_1 = __nccwpck_require__(89040); +const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); +exports.SETUP_AGENT_TASKS = [ + 'planner', + 'findings', + 'reviewer', + 'fixer', + 'tester', + 'release', +]; +exports.SETUP_FEATURE_DESCRIPTIONS = { + issues: 'Issue automation: branching, labels, projects, and issue lifecycle', + pullRequests: 'Pull request automation: review, descriptions, and lifecycle', + commits: 'Commit automation: progress, sizing, and Bugbot analysis', + issueComments: 'Issue comments: questions, translations, and Bugbot autofix', + pullRequestComments: 'Pull request review comments: translations and Bugbot autofix', + release: 'Release workflow: versioning, changelog, tag, and GitHub Release', + hotfix: 'Hotfix workflow: emergency release from a production tag', + agentProvisioning: 'Agent CLI provisioning check workflow', + credentialHealth: 'Read-only remote credential health workflow for setup and doctor', + issueTemplates: 'Issue templates for feature, bug, documentation, and operations', + pullRequestTemplate: 'Pull request template', +}; +const WORKFLOW_FILES = { + issues: ['copilot_issue.yml'], + pullRequests: ['copilot_pull_request.yml'], + commits: ['copilot_commit.yml'], + issueComments: ['copilot_issue_comment.yml'], + pullRequestComments: ['copilot_pull_request_comment.yml'], + release: ['release_workflow.yml'], + hotfix: ['hotfix_workflow.yml'], + agentProvisioning: ['agent-cli-provisioning.yml'], + credentialHealth: ['copilot_credential_health.yml'], +}; +const ISSUE_TEMPLATE_FILES = [ + 'config.yml', + 'feature_request.yml', + 'bug_report.yml', + 'doc_update.yml', + 'chore_task.yml', + 'help_request.yml', + 'hotfix.yml', + 'release.yml', +]; +const SECRET_BY_MODEL_PROVIDER = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + google: 'GOOGLE_API_KEY', + openrouter: 'OPENROUTER_API_KEY', +}; +function createDefaultSetupConfiguration() { + const defaultRole = () => ({ + provider: agent_1.DEFAULT_AGENT_PROVIDER, + modelProvider: agent_1.DEFAULT_MODEL_PROVIDER, + model: agent_1.DEFAULT_AGENT_MODEL, + effort: '', + }); + const agents = Object.fromEntries(exports.SETUP_AGENT_TASKS.map(task => [task, defaultRole()])); + const features = Object.fromEntries(Object.keys(exports.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true])); + return { + features, + agents, + repository: { + mainBranch: 'master', + developmentBranch: 'develop', + featureTree: 'feature', + bugfixTree: 'bugfix', + hotfixTree: 'hotfix', + releaseTree: 'release', + docsTree: 'docs', + choreTree: 'chore', + branchManagementAlways: false, + reopenIssueOnPush: true, + desiredAssigneesCount: 1, + desiredReviewersCount: 1, + mergeTimeout: 600, + issueLocale: 'en-US', + pullRequestLocale: 'en-US', + commitPrefixTransforms: 'replace-slash', + }, + ai: { + pullRequestDescription: true, + ignoreFiles: 'build/*', + membersOnly: false, + includeReasoning: true, + bugbotSeverity: 'low', + bugbotCommentLimit: 20, + bugbotFixVerifyCommands: '', + provisioningMode: 'auto', + }, + projects: { + ids: '', + issueCreatedColumn: 'Todo', + pullRequestCreatedColumn: 'In Progress', + issueInProgressColumn: 'In Progress', + pullRequestInProgressColumn: 'In Progress', + }, + createInitialTag: true, + manageRepositoryVariables: true, + manageRepositorySecrets: true, + actionInputs: {}, + }; +} +function mergeSetupConfiguration(base, overrides = {}) { + const agents = { ...base.agents }; + for (const task of exports.SETUP_AGENT_TASKS) { + agents[task] = { ...base.agents[task], ...(overrides.agents?.[task] ?? {}) }; + } + return { + ...base, + features: { ...base.features, ...(overrides.features ?? {}) }, + agents, + repository: { ...base.repository, ...(overrides.repository ?? {}) }, + ai: { ...base.ai, ...(overrides.ai ?? {}) }, + projects: { ...base.projects, ...(overrides.projects ?? {}) }, + createInitialTag: overrides.createInitialTag ?? base.createInitialTag, + manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, + manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, + actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, + }; +} +function validateSetupConfiguration(configuration) { + const errors = []; + const nonEmpty = [ + ['main branch', configuration.repository.mainBranch], + ['development branch', configuration.repository.developmentBranch], + ['feature branch prefix', configuration.repository.featureTree], + ['bugfix branch prefix', configuration.repository.bugfixTree], + ['hotfix branch prefix', configuration.repository.hotfixTree], + ['release branch prefix', configuration.repository.releaseTree], + ['docs branch prefix', configuration.repository.docsTree], + ['chore branch prefix', configuration.repository.choreTree], + ]; + for (const [name, value] of nonEmpty) { + if (!value.trim() || /\s/.test(value)) + errors.push(`The ${name} must be non-empty and contain no whitespace.`); + } + if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { + errors.push('Desired assignees must be between 0 and 10.'); + } + if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { + errors.push('Desired reviewers must be between 0 and 15.'); + } + if (configuration.repository.mergeTimeout < 0) + errors.push('Merge timeout cannot be negative.'); + if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { + errors.push('Bugbot comment limit must be between 1 and 100.'); + } + if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { + errors.push('Bugbot severity must be info, low, medium, or high.'); + } + if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { + errors.push('Agent provisioning must be auto, always, or disabled.'); + } + for (const task of exports.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) + errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); + if (!agent.modelProvider.trim() || !agent.model.trim()) + errors.push(`Model provider and model are required for ${task}.`); + if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) + errors.push(`Model provider and model for ${task} cannot contain whitespace.`); + } + return errors; +} +function buildSetupPlan(configuration) { + const workflowFiles = Object.entries(WORKFLOW_FILES) + .filter(([feature]) => configuration.features[feature] !== false) + .flatMap(([, files]) => files); + const issueTemplateFiles = configuration.features.issueTemplates === false + ? [] + : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') + .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); + const selectedFiles = [ + ...workflowFiles.map(file => `workflows/${file}`), + ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), + ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), + ]; + return { + configuration, + workflowFiles, + issueTemplateFiles, + selectedFiles, + variables: buildSetupRepositoryVariables(configuration), + requiredSecrets: buildRequiredSetupSecrets(configuration), + credentialRequirements: buildSetupCredentialRequirements(configuration), + warnings: buildSetupWarnings(configuration), + }; +} +/** Builds the non-sensitive credential contract implied by the selected agents. */ +function buildSetupCredentialRequirements(configuration) { + const requirements = new Map(); + const add = (name, kind, description, provider, model) => { + if (!requirements.has(name)) + requirements.set(name, { name, kind, description, provider, model }); + }; + add('PAT', 'workflowPat', 'A separate GitHub token owned by the bot account. It is used by workflows at runtime.'); + for (const task of exports.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (agent.provider === 'cursor') { + add('CURSOR_API_KEY', 'apiKey', 'Cursor API key used by the Cursor agent runtime.', 'cursor', agent.model); + continue; + } + if (agent.provider === 'opencode') + add('OPENCODE_API_KEY', 'apiKey', 'OpenCode API key used by the OpenCode agent runtime.', 'opencode', agent.model); + if (agent.provider === 'codex') + add('CODEX_ACCESS_TOKEN', 'apiKey', 'Codex access token used by the Codex agent runtime.', 'codex', agent.model); + const modelProvider = agent.modelProvider.trim().toLowerCase(); + if (modelProvider && !['local', 'ollama', 'lmstudio'].includes(modelProvider)) { + const name = SECRET_BY_MODEL_PROVIDER[modelProvider] ?? `${modelProvider.replace(/-/g, '_').toUpperCase()}_API_KEY`; + add(name, 'apiKey', `${modelProvider} API key for ${agent.model}.`, modelProvider, agent.model); + } + } + return [...requirements.values()]; +} +function buildSetupRepositoryVariables(configuration) { + const variables = []; + const add = (name, value) => { + if (value === undefined || value === '') + return; + variables.push({ name, value: String(value) }); + }; + const base = configuration.agents.findings; + add('AGENT_PROVIDER', base.provider); + add('AGENT_MODEL_PROVIDER', base.modelProvider); + add('AGENT_MODEL', base.model); + add('AGENT_EFFORT', base.effort); + add('AGENT_PROVISIONING', configuration.ai.provisioningMode); + add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(exports.SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); + add('AGENT_ALLOWED_MODELS', unique(exports.SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); + for (const task of exports.SETUP_AGENT_TASKS) { + const prefix = task.toUpperCase(); + const agent = configuration.agents[task]; + add(`${prefix}_PROVIDER`, agent.provider); + add(`${prefix}_MODEL_PROVIDER`, agent.modelProvider); + add(`${prefix}_MODEL`, agent.model); + add(`${prefix}_EFFORT`, agent.effort); + } + const repository = configuration.repository; + add('MAIN_BRANCH', repository.mainBranch); + add('DEVELOPMENT_BRANCH', repository.developmentBranch); + add('FEATURE_TREE', repository.featureTree); + add('BUGFIX_TREE', repository.bugfixTree); + add('HOTFIX_TREE', repository.hotfixTree); + add('RELEASE_TREE', repository.releaseTree); + add('DOCS_TREE', repository.docsTree); + add('CHORE_TREE', repository.choreTree); + add('BRANCH_MANAGEMENT_ALWAYS', repository.branchManagementAlways); + add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); + add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); + add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); + add('MERGE_TIMEOUT', repository.mergeTimeout); + add('ISSUES_LOCALE', repository.issueLocale); + add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); + add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); + add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); + add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); + add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); + add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); + add('BUGBOT_SEVERITY', configuration.ai.bugbotSeverity); + add('BUGBOT_COMMENT_LIMIT', configuration.ai.bugbotCommentLimit); + add('BUGBOT_AUTOFIX_VERIFY_COMMANDS', configuration.ai.bugbotFixVerifyCommands); + add('PROJECT_IDS', configuration.projects.ids); + add('PROJECT_COLUMN_ISSUE_CREATED', configuration.projects.issueCreatedColumn); + add('PROJECT_COLUMN_PULL_REQUEST_CREATED', configuration.projects.pullRequestCreatedColumn); + add('PROJECT_COLUMN_ISSUE_IN_PROGRESS', configuration.projects.issueInProgressColumn); + add('PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS', configuration.projects.pullRequestInProgressColumn); + return variables; +} +function buildSetupActionInputs(configuration) { + const repository = configuration.repository; + const ai = configuration.ai; + const projects = configuration.projects; + return { + 'main-branch': repository.mainBranch, + 'development-branch': repository.developmentBranch, + 'feature-tree': repository.featureTree, + 'bugfix-tree': repository.bugfixTree, + 'hotfix-tree': repository.hotfixTree, + 'release-tree': repository.releaseTree, + 'docs-tree': repository.docsTree, + 'chore-tree': repository.choreTree, + 'branch-management-always': String(repository.branchManagementAlways), + 'reopen-issue-on-push': String(repository.reopenIssueOnPush), + 'desired-assignees-count': String(repository.desiredAssigneesCount), + 'desired-reviewers-count': String(repository.desiredReviewersCount), + 'merge-timeout': String(repository.mergeTimeout), + 'issues-locale': repository.issueLocale, + 'pull-requests-locale': repository.pullRequestLocale, + 'commit-prefix-transforms': repository.commitPrefixTransforms, + 'ai-pull-request-description': String(ai.pullRequestDescription), + 'ai-ignore-files': ai.ignoreFiles, + 'ai-members-only': String(ai.membersOnly), + 'ai-include-reasoning': String(ai.includeReasoning), + 'bugbot-severity': ai.bugbotSeverity, + 'bugbot-comment-limit': String(ai.bugbotCommentLimit), + 'bugbot-fix-verify-commands': ai.bugbotFixVerifyCommands, + 'project-ids': projects.ids, + 'project-column-issue-created': projects.issueCreatedColumn, + 'project-column-pull-request-created': projects.pullRequestCreatedColumn, + 'project-column-issue-in-progress': projects.issueInProgressColumn, + 'project-column-pull-request-in-progress': projects.pullRequestInProgressColumn, + ...buildAgentActionInputs(configuration), + ...configuration.actionInputs, + }; +} +function buildAgentActionInputs(configuration) { + const result = {}; + const base = configuration.agents.findings; + const add = (key, value) => { if (value !== undefined) + result[key] = value; }; + add('agent-provider', base.provider); + add('agent-model-provider', base.modelProvider); + add('agent-model', base.model); + add('agent-effort', base.effort); + for (const task of exports.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + const prefix = `${task}-`; + add(`${prefix}provider`, agent.provider); + add(`${prefix}model-provider`, agent.modelProvider); + add(`${prefix}model`, agent.model); + add(`${prefix}effort`, agent.effort); + } + return result; +} +function buildRequiredSetupSecrets(configuration) { + return buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); +} +function buildSetupWarnings(configuration) { + const warnings = []; + if (configuration.features.release !== false && configuration.features.hotfix !== false) { + warnings.push('Release and hotfix workflows require the repository secret PAT and a writable token.'); + } + if (configuration.ai.provisioningMode === 'always') { + warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); + } + if (configuration.projects.ids.trim()) { + warnings.push('Project IDs must be accessible to the PAT and use the expected project column names.'); + } + if (exports.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { + warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); + } + return warnings; +} +function unique(values) { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} + + /***/ }), /***/ 43193: @@ -55304,7 +57674,7 @@ exports.InitialSetupUseCase = void 0; const initial_setup_workflow_1 = __nccwpck_require__(18079); /** Application boundary for provisioning a repository for Copilot automation. */ class InitialSetupUseCase { - constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort) { + constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort) { this.authenticatedUserPort = authenticatedUserPort; this.initialLabelProvisioningPort = initialLabelProvisioningPort; this.issueTypeProvisioningPort = issueTypeProvisioningPort; @@ -55312,6 +57682,8 @@ class InitialSetupUseCase { this.repositoryDefaultBranchPort = repositoryDefaultBranchPort; this.repositoryTagPort = repositoryTagPort; this.setupWorkspacePort = setupWorkspacePort; + this.setupRepositoryVariablesPort = setupRepositoryVariablesPort; + this.setupRepositorySecretsPort = setupRepositorySecretsPort; this.taskId = 'InitialSetupUseCase'; } async invoke(param) { @@ -55323,6 +57695,8 @@ class InitialSetupUseCase { repositoryDefaultBranchPort: this.repositoryDefaultBranchPort, repositoryTagPort: this.repositoryTagPort, setupWorkspacePort: this.setupWorkspacePort, + setupRepositoryVariablesPort: this.setupRepositoryVariablesPort, + setupRepositorySecretsPort: this.setupRepositorySecretsPort, }); } } @@ -55342,6 +57716,7 @@ const result_1 = __nccwpck_require__(73817); const version_policy_1 = __nccwpck_require__(8381); const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); +const setup_configuration_policy_1 = __nccwpck_require__(56637); const TASK_ID = 'InitialSetupUseCase'; /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ async function runInitialSetupWorkflow(param, dependencies) { @@ -55349,14 +57724,23 @@ async function runInitialSetupWorkflow(param, dependencies) { const steps = []; const errors = []; try { - (0, logging_ports_1.logInfo)('📋 Ensuring .github and copying setup files...'); - const filesResult = dependencies.setupWorkspacePort.prepare(); - steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); - if (!dependencies.setupWorkspacePort.hasValidToken()) { - (0, logging_ports_1.logInfo)(' 🛑 Setup requires PERSONAL_ACCESS_TOKEN (environment or .env) with a valid token.'); - errors.push('PERSONAL_ACCESS_TOKEN must be set (environment or .env) with a valid token to run setup.'); + const setupConfiguration = getSetupConfiguration(param); + if (!dependencies.setupWorkspacePort.hasValidToken(param.tokens.token)) { + (0, logging_ports_1.logInfo)(' 🛑 Setup requires the setup PAT provided for this command with a valid token.'); + errors.push('A valid setup PAT must be provided to run setup. It is separate from the workflow PAT Secret.'); return [buildResult(errors, steps)]; } + (0, logging_ports_1.logInfo)('📋 Ensuring .github and copying setup files...'); + const workflowUpdates = getWorkflowUpdates(param); + const workspaceSelection = { + features: setupConfiguration?.features, + ...(workflowUpdates.length > 0 ? { + updateExistingWorkflows: true, + approvedWorkflowFiles: workflowUpdates, + } : {}), + }; + const filesResult = dependencies.setupWorkspacePort.prepare(workspaceSelection); + steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); (0, logging_ports_1.logInfo)('🔐 Checking GitHub access...'); const githubAccess = await verifyGitHubAccess(param, dependencies.authenticatedUserPort); if (!githubAccess.success) { @@ -55364,6 +57748,11 @@ async function runInitialSetupWorkflow(param, dependencies) { return [buildResult(errors, steps)]; } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); + const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration); + if (secrets.step) + steps.push(secrets.step); + if (secrets.errors.length > 0) + errors.push(...secrets.errors); (0, logging_ports_1.logInfo)('🏷️ Checking configured and progress labels...'); const labels = await ensureInitialLabels(param, dependencies.initialLabelProvisioningPort); if (!labels.completed) { @@ -55381,7 +57770,12 @@ async function runInitialSetupWorkflow(param, dependencies) { else { steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const defaultVersion = await ensureDefaultVersion(param, dependencies); + const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration); + if (variables.step) + steps.push(variables.step); + if (variables.errors.length > 0) + errors.push(...variables.errors); + const defaultVersion = await ensureDefaultVersion(param, dependencies, setupConfiguration); if (defaultVersion.step) steps.push(defaultVersion.step); if (defaultVersion.error) @@ -55430,7 +57824,10 @@ async function ensureIssueTypes(param, repository) { return { success: false, created: 0, existing: 0, errors: [`Error ensuring issue types: ${error}`] }; } } -async function ensureDefaultVersion(param, dependencies) { +async function ensureDefaultVersion(param, dependencies, setupConfiguration) { + if (setupConfiguration?.createInitialTag === false) { + return { step: '⏭️ Initial version tag creation disabled by setup configuration.' }; + } try { const existingTag = await dependencies.latestTagQueryPort.getLatestTag(); if (existingTag !== undefined) { @@ -55455,6 +57852,70 @@ async function ensureDefaultVersion(param, dependencies) { return { error: message }; } } +function getSetupConfiguration(param) { + const configuration = param.inputs?.setupConfiguration; + return configuration && typeof configuration === 'object' + ? configuration + : undefined; +} +function getWorkflowUpdates(param) { + const updates = param.inputs?.setupWorkflowUpdates; + return Array.isArray(updates) ? updates.filter((file) => typeof file === 'string') : []; +} +async function ensureRepositoryVariables(param, dependencies, setupConfiguration) { + if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { + return { errors: [] }; + } + try { + const result = await dependencies.setupRepositoryVariablesPort.upsert(param.owner, param.repo, param.tokens.token, (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration)); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ Repository Variables: ${result.created} created, ${result.updated} updated`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Variables: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +async function ensureRepositorySecrets(param, dependencies, setupConfiguration) { + if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { + return { errors: [] }; + } + const credentials = getSetupCredentialCollection(param); + if (!credentials) { + return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; + } + const values = [ + ...(credentials.workflowPat ? [credentials.workflowPat] : []), + ...credentials.apiKeys, + ]; + if (values.length === 0) + return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; + try { + const result = await dependencies.setupRepositorySecretsPort.upsertSecrets(param.owner, param.repo, param.tokens.token, values); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ Repository Secrets: ${result.created} created, ${result.updated} updated; existing values kept when no replacement was selected.`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Secrets: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +function getSetupCredentialCollection(param) { + const credentials = param.inputs?.setupCredentials; + if (!credentials || typeof credentials !== 'object') + return undefined; + return credentials; +} function appendLabelSummary(steps, errors, summary, labelType) { if (summary.errors.length > 0) { errors.push(...summary.errors); @@ -56940,6 +59401,223 @@ function logPullRequestState(param) { } +/***/ }), + +/***/ 87328: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SetupDoctorUseCase = void 0; +const setup_configuration_policy_1 = __nccwpck_require__(56637); +class SetupDoctorUseCase { + constructor(validation, secrets, variables, workspace, output, remoteHealth) { + this.validation = validation; + this.secrets = secrets; + this.variables = variables; + this.workspace = workspace; + this.output = output; + this.remoteHealth = remoteHealth; + } + async execute(request) { + const checks = []; + const pat = await this.validation.validateSetupPat(request.owner, request.repository, request.setupToken); + checks.push({ area: 'Setup PAT', status: pat.status === 'valid' ? 'pass' : 'fail', message: pat.message }); + if (pat.status !== 'valid') { + this.output.showDoctorChecks(checks); + return false; + } + const comparisons = this.workspace.compareWorkflows?.(request.configuration.features) ?? []; + for (const comparison of comparisons) { + checks.push({ + area: `Workflow ${comparison.file}`, + status: comparison.status === 'unchanged' ? 'pass' : 'fail', + message: comparison.status === 'unchanged' ? 'Matches the installed setup template.' : `Local workflow is ${comparison.status}.`, + }); + } + const requiredVariables = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(request.configuration); + const remoteVariables = await this.variables.listVariables(request.owner, request.repository, request.setupToken); + const remoteVariableMap = new Map(remoteVariables.map(variable => [variable.name, variable.value])); + for (const variable of requiredVariables) { + const value = remoteVariableMap.get(variable.name); + checks.push({ + area: `Variable ${variable.name}`, + status: value === undefined ? 'fail' : value === variable.value ? 'pass' : 'fail', + message: value === undefined ? 'Variable is missing.' : value === variable.value ? 'Variable is configured.' : 'Variable exists but differs from the selected setup configuration.', + }); + } + const remoteSecrets = new Set(await this.secrets.list(request.owner, request.repository, request.setupToken)); + const requirements = (0, setup_configuration_policy_1.buildSetupCredentialRequirements)(request.configuration); + const remoteHealth = this.remoteHealth + ? await this.remoteHealth.validateExisting(request.owner, request.repository, request.setupToken, request.configuration.repository.mainBranch, requirements.filter(requirement => remoteSecrets.has(requirement.name))) + : undefined; + const remoteHealthByName = new Map((remoteHealth ?? []).map(check => [check.name, check])); + for (const requirement of requirements) { + if (!remoteSecrets.has(requirement.name)) { + checks.push({ area: `Secret ${requirement.name}`, status: 'fail', message: 'Secret is missing.' }); + } + else { + const health = remoteHealthByName.get(requirement.name); + checks.push({ + area: `Secret ${requirement.name}`, + status: health?.status === 'valid' ? 'pass' : health?.status === 'invalid' ? 'fail' : 'warn', + message: health?.message ?? 'Secret is present, but the remote credential health workflow is unavailable.', + }); + } + } + this.output.showDoctorChecks(checks); + return checks.every(check => check.status !== 'fail'); + } +} +exports.SetupDoctorUseCase = SetupDoctorUseCase; + + +/***/ }), + +/***/ 36888: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SetupCredentialsUseCase = exports.SetupWizardUseCase = void 0; +var setup_wizard_use_case_1 = __nccwpck_require__(43433); +Object.defineProperty(exports, "SetupWizardUseCase", ({ enumerable: true, get: function () { return setup_wizard_use_case_1.SetupWizardUseCase; } })); +var setup_credentials_use_case_1 = __nccwpck_require__(67438); +Object.defineProperty(exports, "SetupCredentialsUseCase", ({ enumerable: true, get: function () { return setup_credentials_use_case_1.SetupCredentialsUseCase; } })); + + +/***/ }), + +/***/ 67438: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SetupCredentialsUseCase = void 0; +/** Coordinates secret collection and validation without placing secret values in config files. */ +class SetupCredentialsUseCase { + constructor(prompt, validation, secrets, remoteHealth) { + this.prompt = prompt; + this.validation = validation; + this.secrets = secrets; + this.remoteHealth = remoteHealth; + } + async collect(request) { + const setupCheck = await this.validation.validateSetupPat(request.owner, request.repository, request.setupToken); + if (setupCheck.status !== 'valid') { + throw new Error(`Setup PAT validation failed: ${setupCheck.message}`); + } + if (!request.manageSecrets) { + this.prompt.showCredentialChecks([setupCheck]); + return { collection: { apiKeys: [] }, checks: [setupCheck], existingSecretNames: [] }; + } + if (!this.secrets) + throw new Error('Repository Secret provisioning is not available in this installation.'); + const existingSecretNames = await this.secrets.list(request.owner, request.repository, request.setupToken); + const requirements = request.requirements.filter(requirement => requirement.name !== 'SETUP_PAT'); + this.prompt.explainCredentialSeparation(requirements); + const existingRequirements = requirements.filter(requirement => existingSecretNames.includes(requirement.name)); + const remoteChecks = this.remoteHealth && existingRequirements.length > 0 + ? await this.remoteHealth.validateExisting(request.owner, request.repository, request.setupToken, request.ref ?? 'master', existingRequirements) + : undefined; + const remoteCheckByName = new Map((remoteChecks ?? []).map(check => [check.name, check])); + const checks = [setupCheck]; + const values = []; + for (const requirement of requirements) { + const existing = existingSecretNames.includes(requirement.name); + if (existing) { + const remoteCheck = remoteCheckByName.get(requirement.name) ?? { + name: requirement.name, + status: 'unverifiable', + message: 'The remote health workflow is not available yet; GitHub does not reveal Secret values.', + }; + checks.push(remoteCheck); + const decision = await this.prompt.chooseExistingCredential(requirement, remoteCheck); + if (remoteCheck.status === 'invalid' && decision !== 'replace') { + throw new Error(`${requirement.name} is invalid and must be replaced before setup can continue.`); + } + if (decision === 'keep') + continue; + if (decision === 'skip') + continue; + } + const value = requirement.kind === 'workflowPat' + ? await this.prompt.requestWorkflowPat(requirement, existing ? checks[checks.length - 1] : undefined) + : await this.prompt.requestApiKey(requirement, existing ? checks[checks.length - 1] : undefined); + if (!value) { + if (!existing) + checks.push({ name: requirement.name, status: 'missing', message: 'No value was provided.' }); + throw new Error(`${requirement.name} is required by the selected workflows.`); + } + const check = requirement.kind === 'workflowPat' + ? await this.validation.validateSetupPat(request.owner, request.repository, value.value) + : await this.validation.validateCredential(requirement, value.value); + checks.push({ ...check, name: requirement.name }); + if (check.status !== 'valid') { + throw new Error(`${requirement.name} validation failed: ${check.message}`); + } + values.push(value); + } + this.prompt.showCredentialChecks(checks); + return { + collection: { + workflowPat: values.find(value => value.name === 'PAT'), + apiKeys: values.filter(value => value.name !== 'PAT'), + }, + checks, + existingSecretNames, + }; + } +} +exports.SetupCredentialsUseCase = SetupCredentialsUseCase; + + +/***/ }), + +/***/ 43433: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SetupWizardUseCase = void 0; +const setup_configuration_policy_1 = __nccwpck_require__(56637); +class SetupWizardUseCase { + constructor(prompt) { + this.prompt = prompt; + } + async collect(request = {}) { + const defaults = (0, setup_configuration_policy_1.mergeSetupConfiguration)((0, setup_configuration_policy_1.createDefaultSetupConfiguration)(), { + ...request.overrides, + ...(request.skipRepositoryVariables ? { manageRepositoryVariables: false } : {}), + }); + const collected = await this.prompt.collect(defaults); + const configuration = request.skipRepositoryVariables + ? { ...collected, manageRepositoryVariables: false } + : collected; + const validationErrors = (0, setup_configuration_policy_1.validateSetupConfiguration)(configuration); + if (validationErrors.length > 0) { + throw new Error(`Invalid setup configuration:\n${validationErrors.map(error => `- ${error}`).join('\n')}`); + } + const plan = (0, setup_configuration_policy_1.buildSetupPlan)(configuration); + this.prompt.showPlan(plan); + if (!(await this.prompt.confirm(plan))) + return undefined; + return configuration; + } + plan(configuration) { + return (0, setup_configuration_policy_1.buildSetupPlan)(configuration); + } + close() { + this.prompt.close(); + } +} +exports.SetupWizardUseCase = SetupWizardUseCase; + + /***/ }), /***/ 73572: @@ -62958,10 +65636,8 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createCliProgram = createCliProgram; const node_fs_1 = __nccwpck_require__(87561); const path = __importStar(__nccwpck_require__(49411)); -const dotenv = __importStar(__nccwpck_require__(11406)); const commander_1 = __nccwpck_require__(12239); const command_registry_1 = __nccwpck_require__(94415); -dotenv.config(); function loadPackageVersion() { const packagePath = path.join(__dirname, '..', '..', 'package.json'); const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packagePath, 'utf8')); @@ -63021,6 +65697,7 @@ const recommend_steps_1 = __nccwpck_require__(91523); const detect_potential_problems_1 = __nccwpck_require__(70850); const setup_1 = __nccwpck_require__(32139); const upgrade_1 = __nccwpck_require__(27087); +const doctor_1 = __nccwpck_require__(74364); function registerCliCommands(program) { (0, think_1.registerThinkCommand)(program); (0, do_1.registerDoCommand)(program); @@ -63029,6 +65706,7 @@ function registerCliCommands(program) { (0, detect_potential_problems_1.registerDetectPotentialProblemsCommand)(program); (0, setup_1.registerSetupCommand)(program); (0, upgrade_1.registerUpgradeCommand)(program); + (0, doctor_1.registerDoctorCommand)(program); return program; } @@ -63402,6 +66080,66 @@ function formatDoResponse(text, sessionId, outputFormat) { } +/***/ }), + +/***/ 74364: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.registerDoctorCommand = registerDoctorCommand; +const cli_context_1 = __nccwpck_require__(21307); +const setup_files_1 = __nccwpck_require__(59126); +const logger_1 = __nccwpck_require__(91151); +const setup_prompt_adapter_1 = __nccwpck_require__(82703); +const setup_doctor_composition_root_1 = __nccwpck_require__(56360); +const setup_config_file_1 = __nccwpck_require__(11196); +const setup_configuration_policy_1 = __nccwpck_require__(56637); +function registerDoctorCommand(program) { + program + .command('doctor') + .description('Verify Copilot workflows, Variables, Secrets, and setup PAT without changing repository configuration') + .option('-t, --token ', 'Setup PAT (or PERSONAL_ACCESS_TOKEN from the environment)') + .option('--config ', 'YAML or JSON setup configuration used as the expected contract') + .option('--non-interactive', 'Do not prompt; use --token or PERSONAL_ACCESS_TOKEN', false) + .action(async (options) => { + const prompt = new setup_prompt_adapter_1.SetupPromptAdapter({ interactive: !options.nonInteractive }); + try { + const cwd = process.cwd(); + if (!(0, cli_context_1.isInsideGitRepo)(cwd)) + throw new Error('Run "copilot doctor" from the root of a git repository.'); + const gitInfo = (0, cli_context_1.getGitInfo)(); + if ('error' in gitInfo) + throw new Error(gitInfo.error); + let token = (0, setup_files_1.getSetupToken)(cwd, options.token); + if (!token && !options.nonInteractive) + token = await prompt.requestSetupPat(); + if (!token) + throw new Error('A setup PAT is required. Use --token or PERSONAL_ACCESS_TOKEN. No .env file is supported.'); + const overrides = options.config ? (0, setup_config_file_1.loadSetupConfigurationOverrides)(options.config) : {}; + const expected = (0, setup_configuration_policy_1.mergeSetupConfiguration)((0, setup_configuration_policy_1.createDefaultSetupConfiguration)(), overrides); + (0, logger_1.logInfo)(`🩺 Checking Copilot configuration for ${gitInfo.owner}/${gitInfo.repo}...`); + const healthy = await (0, setup_doctor_composition_root_1.createSetupDoctorUseCase)(prompt).execute({ + owner: gitInfo.owner, + repository: gitInfo.repo, + setupToken: token, + configuration: expected, + }); + if (!healthy) + process.exitCode = 1; + } + catch (error) { + (0, logger_1.logError)(`Doctor failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + } + finally { + prompt.close(); + } + }); +} + + /***/ }), /***/ 66915: @@ -63505,10 +66243,43 @@ function registerRecommendStepsCommand(program) { /***/ }), /***/ 32139: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.registerSetupCommand = registerSetupCommand; const local_action_1 = __nccwpck_require__(76102); @@ -63517,47 +66288,153 @@ const setup_files_1 = __nccwpck_require__(59126); const logger_1 = __nccwpck_require__(91151); const cli_context_1 = __nccwpck_require__(21307); const setup_policy_1 = __nccwpck_require__(28732); +const setup_config_file_1 = __nccwpck_require__(11196); +const setup_1 = __nccwpck_require__(36888); +const setup_configuration_policy_1 = __nccwpck_require__(56637); +const setup_credentials_composition_root_1 = __nccwpck_require__(69084); +const setup_workspace_adapter_1 = __nccwpck_require__(5729); function registerSetupCommand(program) { program .command('setup') - .description(`${constants_1.TITLE} - Initial setup: create labels, issue types, and verify access`) + .description(`${constants_1.TITLE} - Interactive repository setup: select workflows, agents, Variables, labels, and issue types`) .option('-d, --debug', 'Debug mode', false) .option('-t, --token ', 'Personal access token (or PERSONAL_ACCESS_TOKEN from the environment)') + .option('--agent ', 'Use one agent runtime for every setup task (codex|opencode|cursor)') + .option('--features ', 'Comma-separated setup features, or "all" (for non-interactive setup)') + .option('--config ', 'YAML or JSON file with setup overrides') + .option('--non-interactive', 'Use defaults and config-file values without prompting', false) + .option('--yes', 'Apply the plan without the final confirmation prompt', false) + .option('--dry-run', 'Show the setup plan without changing files or GitHub', false) + .option('--skip-variables', 'Do not create or update GitHub Repository Variables', false) + .option('--skip-secrets', 'Do not validate or create/update GitHub Repository Secrets', false) + .option('--update-workflows', 'Allow setup-managed workflows already in the repository to be updated', false) + .option('--workflow-pat ', 'Workflow PAT for the bot account (prefer the hidden interactive prompt)') + .option('--secret ', 'Secret value for non-interactive setup; repeat for each API key', collectSecret, {}) .action(async (options) => { + const { SetupPromptAdapter } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(82703))); + const prompt = new SetupPromptAdapter({ + interactive: !options.nonInteractive, + assumeYes: Boolean(options.yes || options.nonInteractive || options.dryRun), + credentialValues: { + ...(options.workflowPat ? { PAT: options.workflowPat } : {}), + ...options.secret, + }, + }); const cwd = process.cwd(); - (0, logger_1.logInfo)('🔍 Checking we are inside a git repository...'); - if (!(0, cli_context_1.isInsideGitRepo)(cwd)) { - (0, logger_1.logError)('❌ Not a git repository. Run "copilot setup" from the root of a git repo.'); - process.exit(1); + try { + (0, logger_1.logInfo)('🔍 Checking we are inside a git repository...'); + if (!(0, cli_context_1.isInsideGitRepo)(cwd)) { + (0, logger_1.logError)('❌ Not a git repository. Run "copilot setup" from the root of a git repo.'); + process.exit(1); + return; + } + (0, logger_1.logInfo)('✅ Git repository detected.'); + (0, logger_1.logInfo)('🔗 Resolving repository (owner/repo)...'); + const gitInfo = (0, cli_context_1.getGitInfo)(); + if ('error' in gitInfo) { + (0, logger_1.logError)(gitInfo.error); + process.exit(1); + return; + } + (0, logger_1.logInfo)(`📦 Repository: ${gitInfo.owner}/${gitInfo.repo}`); + let token = (0, setup_files_1.getSetupToken)(cwd, options.token); + if (!token && !options.nonInteractive && !options.dryRun) + token = await prompt.requestSetupPat(); + if (!token && !options.dryRun) { + (0, logger_1.logError)('🛑 Setup requires PERSONAL_ACCESS_TOKEN with a valid token.'); + (0, logger_1.logInfo)(' You can:'); + (0, logger_1.logInfo)(' • Pass it on the command line: copilot setup --token '); + (0, logger_1.logInfo)(' • Add it to your environment: export PERSONAL_ACCESS_TOKEN=your_github_token'); + process.exit(1); + return; + } + (0, logger_1.logInfo)(options.dryRun ? '🧭 Building a dry-run setup plan...' : '🧭 Building your setup plan...'); + const wizard = new setup_1.SetupWizardUseCase(prompt); + const overrides = loadSetupOverrides(options); + const configuration = await wizard.collect({ + overrides, + skipRepositoryVariables: Boolean(options.skipVariables), + }); + if (!configuration) { + (0, logger_1.logInfo)('⏭️ Setup cancelled. No changes were applied.'); + return; + } + const workflowComparisons = new setup_workspace_adapter_1.SetupWorkspaceAdapter().compareWorkflows(configuration.features); + const updateWorkflows = await prompt.confirmWorkflowUpdates(workflowComparisons, Boolean(options.updateWorkflows)); + const approvedWorkflowFiles = updateWorkflows + ? workflowComparisons.filter(comparison => comparison.status === 'changed').map(comparison => comparison.file) + : []; + if (options.dryRun) { + (0, logger_1.logInfo)('✅ Dry run complete. No files or GitHub resources were changed.'); + return; + } + const credentials = await (0, setup_credentials_composition_root_1.createSetupCredentialsUseCase)(prompt).collect({ + owner: gitInfo.owner, + repository: gitInfo.repo, + setupToken: token ?? '', + requirements: (0, setup_configuration_policy_1.buildSetupCredentialRequirements)(configuration), + manageSecrets: !options.skipSecrets && configuration.manageRepositorySecrets, + ref: configuration.repository.mainBranch, + }); + (0, logger_1.logInfo)('⚙️ Applying the approved setup plan...'); + const params = (0, setup_policy_1.buildSetupParams)(options, gitInfo, token ?? '', configuration, credentials.collection, approvedWorkflowFiles); + if (!params) + return; + await (0, local_action_1.runLocalAction)(params); } - (0, logger_1.logInfo)('✅ Git repository detected.'); - (0, logger_1.logInfo)('🔗 Resolving repository (owner/repo)...'); - const gitInfo = (0, cli_context_1.getGitInfo)(); - if ('error' in gitInfo) { - (0, logger_1.logError)(gitInfo.error); - process.exit(1); + catch (error) { + (0, logger_1.logError)(`Setup failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; } - (0, logger_1.logInfo)(`📦 Repository: ${gitInfo.owner}/${gitInfo.repo}`); - const token = (0, setup_files_1.getSetupToken)(cwd, options.token); - if (!token) { - (0, logger_1.logError)('🛑 Setup requires PERSONAL_ACCESS_TOKEN with a valid token.'); - (0, logger_1.logInfo)(' You can:'); - (0, logger_1.logInfo)(' • Pass it on the command line: copilot setup --token '); - (0, logger_1.logInfo)(' • Add it to your environment: export PERSONAL_ACCESS_TOKEN=your_github_token'); - if ((0, setup_files_1.setupEnvFileExists)(cwd)) - (0, logger_1.logInfo)(' • Or add PERSONAL_ACCESS_TOKEN=your_github_token to your existing .env file'); - else - (0, logger_1.logInfo)(' • Or create a .env file in this repo with: PERSONAL_ACCESS_TOKEN=your_github_token'); - process.exit(1); - return; + finally { + prompt.close(); } - (0, logger_1.logInfo)('⚙️ Running initial setup (labels, issue types, access)...'); - const params = (0, setup_policy_1.buildSetupParams)(options, gitInfo, token); - if (!params) - return; - await (0, local_action_1.runLocalAction)(params); }); } +function collectSecret(value, previous) { + const separator = value.indexOf('='); + if (separator <= 0) + throw new Error('--secret must use NAME=VALUE syntax.'); + const name = value.slice(0, separator).trim(); + const secret = value.slice(separator + 1); + if (!/^[A-Z][A-Z0-9_]*$/.test(name) || !secret) + throw new Error('--secret must use a non-empty NAME=VALUE with an uppercase secret name.'); + return { ...previous, [name]: secret }; +} +function loadSetupOverrides(options) { + const fromFile = options.config ? (0, setup_config_file_1.loadSetupConfigurationOverrides)(options.config) : {}; + const fromFlags = {}; + if (options.agent) { + if (!['codex', 'opencode', 'cursor'].includes(options.agent)) { + throw new Error('--agent must be one of: codex, opencode, cursor.'); + } + fromFlags.agents = Object.fromEntries(['planner', 'findings', 'reviewer', 'fixer', 'tester', 'release'].map(task => [task, { provider: options.agent }])); + } + if (options.features) { + if (options.features.trim().toLowerCase() === 'all') { + fromFlags.features = Object.fromEntries(Object.keys(setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true])); + } + else { + const requested = options.features.split(',').map(feature => feature.trim()).filter(Boolean); + const unknown = requested.filter(feature => !Object.prototype.hasOwnProperty.call(setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS, feature)); + if (unknown.length > 0) + throw new Error(`Unknown setup feature(s): ${unknown.join(', ')}.`); + fromFlags.features = Object.fromEntries(Object.keys(setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, requested.includes(feature)])); + } + } + return mergeSetupOverrides(fromFile, fromFlags); +} +function mergeSetupOverrides(fileOverrides, flagOverrides) { + return { + ...fileOverrides, + ...flagOverrides, + features: { ...fileOverrides.features, ...flagOverrides.features }, + agents: { ...fileOverrides.agents, ...flagOverrides.agents }, + repository: { ...fileOverrides.repository, ...flagOverrides.repository }, + ai: { ...fileOverrides.ai, ...flagOverrides.ai }, + projects: { ...fileOverrides.projects, ...flagOverrides.projects }, + }; +} /***/ }), @@ -63570,10 +66447,12 @@ function registerSetupCommand(program) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildSetupParams = buildSetupParams; const constants_1 = __nccwpck_require__(15415); -function buildSetupParams(options, gitInfo, token) { +const setup_configuration_policy_1 = __nccwpck_require__(56637); +function buildSetupParams(options, gitInfo, token, configuration, credentials, approvedWorkflowFiles = []) { if ('error' in gitInfo) return undefined; return { + ...(configuration ? (0, setup_configuration_policy_1.buildSetupActionInputs)(configuration) : {}), [constants_1.INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', [constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.INITIAL_SETUP, [constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: 1, @@ -63583,8 +66462,11 @@ function buildSetupParams(options, gitInfo, token) { [constants_1.INPUT_KEYS.WELCOME_TITLE]: '⚙️ Initial Setup', [constants_1.INPUT_KEYS.WELCOME_MESSAGES]: [ `Running initial setup for ${gitInfo.owner}/${gitInfo.repo}...`, - 'This will create labels, issue types, and verify access to GitHub.', + 'This will install the selected workflows, configure repository Variables, create labels and issue types, and verify access to GitHub.', ], + ...(configuration ? { setupConfiguration: configuration } : {}), + ...(credentials ? { setupCredentials: credentials } : {}), + setupWorkflowUpdates: approvedWorkflowFiles, }; } @@ -63717,6 +66599,480 @@ function registerUpgradeCommand(program) { } +/***/ }), + +/***/ 11196: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.loadSetupConfigurationOverrides = loadSetupConfigurationOverrides; +const node_fs_1 = __nccwpck_require__(87561); +const yaml = __importStar(__nccwpck_require__(78270)); +const setup_configuration_policy_1 = __nccwpck_require__(56637); +const SETUP_OVERRIDE_KEYS = new Set([ + 'features', + 'agents', + 'repository', + 'ai', + 'projects', + 'createInitialTag', + 'manageRepositoryVariables', + 'manageRepositorySecrets', + 'actionInputs', +]); +const AGENT_OVERRIDE_KEYS = new Set(['provider', 'modelProvider', 'model', 'effort']); +const REPOSITORY_STRING_KEYS = new Set([ + 'mainBranch', + 'developmentBranch', + 'featureTree', + 'bugfixTree', + 'hotfixTree', + 'releaseTree', + 'docsTree', + 'choreTree', + 'issueLocale', + 'pullRequestLocale', + 'commitPrefixTransforms', +]); +const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush']); +const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout']); +const AI_BOOLEAN_KEYS = new Set(['pullRequestDescription', 'membersOnly', 'includeReasoning']); +const AI_STRING_KEYS = new Set(['ignoreFiles', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); +const AI_NUMBER_KEYS = new Set(['bugbotCommentLimit']); +const PROJECT_KEYS = new Set([ + 'ids', + 'issueCreatedColumn', + 'pullRequestCreatedColumn', + 'issueInProgressColumn', + 'pullRequestInProgressColumn', +]); +/** Loads a non-secret setup override file. JSON and YAML are supported. */ +function loadSetupConfigurationOverrides(filePath) { + const parsed = yaml.load((0, node_fs_1.readFileSync)(filePath, 'utf8')); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Setup configuration must be a YAML or JSON object.'); + } + const raw = parsed; + if (containsCredentialMaterial(raw)) { + throw new Error('Setup configuration must not contain secrets or credential material.'); + } + validateObjectKeys(raw, SETUP_OVERRIDE_KEYS, 'setup configuration'); + validateOptionalObject(raw.features, 'features'); + if (raw.features !== undefined) { + validateObjectKeys(raw.features, new Set(Object.keys(setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS)), 'features'); + validateBooleanValues(raw.features, 'features'); + } + validateOptionalObject(raw.agents, 'agents'); + if (raw.agents !== undefined) { + const agents = raw.agents; + validateObjectKeys(agents, new Set(setup_configuration_policy_1.SETUP_AGENT_TASKS), 'agents'); + for (const [task, value] of Object.entries(agents)) { + validateObject(value, `agents.${task}`); + const agent = value; + validateObjectKeys(agent, AGENT_OVERRIDE_KEYS, `agents.${task}`); + validateStringValues(agent, `agents.${task}`); + } + } + validateSection(raw.repository, 'repository', REPOSITORY_STRING_KEYS, REPOSITORY_BOOLEAN_KEYS, REPOSITORY_NUMBER_KEYS); + validateSection(raw.ai, 'ai', AI_STRING_KEYS, AI_BOOLEAN_KEYS, AI_NUMBER_KEYS); + validateSection(raw.projects, 'projects', PROJECT_KEYS, new Set(), new Set()); + validateBooleanProperty(raw, 'createInitialTag'); + validateBooleanProperty(raw, 'manageRepositoryVariables'); + validateBooleanProperty(raw, 'manageRepositorySecrets'); + validateOptionalObject(raw.actionInputs, 'actionInputs'); + if (raw.actionInputs !== undefined) + validateStringValues(raw.actionInputs, 'actionInputs'); + return raw; +} +function validateSection(value, name, stringKeys, booleanKeys, numberKeys) { + if (value === undefined) + return; + validateObject(value, name); + const section = value; + validateObjectKeys(section, new Set([...stringKeys, ...booleanKeys, ...numberKeys]), name); + for (const key of stringKeys) + if (section[key] !== undefined && typeof section[key] !== 'string') + throw new Error(`${name}.${key} must be a string.`); + for (const key of booleanKeys) + if (section[key] !== undefined && typeof section[key] !== 'boolean') + throw new Error(`${name}.${key} must be a boolean.`); + for (const key of numberKeys) + if (section[key] !== undefined && (!Number.isInteger(section[key]) || section[key] < 0)) + throw new Error(`${name}.${key} must be a non-negative integer.`); +} +function validateOptionalObject(value, name) { + if (value !== undefined) + validateObject(value, name); +} +function validateObject(value, name) { + if (!value || typeof value !== 'object' || Array.isArray(value)) + throw new Error(`${name} must be an object.`); +} +function validateObjectKeys(value, allowed, name) { + const unknown = Object.keys(value).filter(key => !allowed.has(key)); + if (unknown.length > 0) + throw new Error(`Unknown ${name} field(s): ${unknown.join(', ')}.`); +} +function validateBooleanValues(value, name) { + for (const [key, item] of Object.entries(value)) + if (typeof item !== 'boolean') + throw new Error(`${name}.${key} must be a boolean.`); +} +function validateStringValues(value, name) { + for (const [key, item] of Object.entries(value)) + if (typeof item !== 'string') + throw new Error(`${name}.${key} must be a string.`); +} +function validateBooleanProperty(value, key) { + if (value[key] !== undefined && typeof value[key] !== 'boolean') + throw new Error(`${key} must be a boolean.`); +} +function containsCredentialMaterial(value) { + if (typeof value === 'string') { + return /^(?:github_pat_|gh[pso]_|ghu_|ghs_|sk-|AIza|xox[baprs]-)/i.test(value.trim()); + } + if (!value || typeof value !== 'object') + return false; + if (Array.isArray(value)) + return value.some(containsCredentialMaterial); + return Object.entries(value).some(([key, item]) => { + // Boolean configuration switches such as `manageRepositorySecrets` and + // `features.credentialHealth` are not credential material. Only reject + // credential-shaped properties when they actually carry a value. + const looksLikeCredentialProperty = /(?:password|secret|token|api[_-]?key|credential)/i.test(key); + return (looksLikeCredentialProperty && item !== undefined && item !== null && typeof item !== 'boolean') + || containsCredentialMaterial(item); + }); +} + + +/***/ }), + +/***/ 82703: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SetupPromptAdapter = void 0; +const promises_1 = __nccwpck_require__(32887); +const node_process_1 = __nccwpck_require__(97742); +const setup_configuration_policy_1 = __nccwpck_require__(56637); +const AGENT_PROVIDERS = ['codex', 'opencode', 'cursor']; +const MODEL_PROVIDERS = ['openai', 'anthropic', 'google', 'openrouter', 'opencode', 'local']; +class SetupPromptAdapter { + constructor(options = {}) { + this.interactive = Boolean((options.interactive ?? Boolean(node_process_1.stdin.isTTY && node_process_1.stdout.isTTY)) + && node_process_1.stdin.isTTY + && node_process_1.stdout.isTTY + && !process.env.JEST_WORKER_ID); + this.assumeYes = options.assumeYes ?? false; + this.credentialValues = options.credentialValues ?? {}; + this.readline = this.interactive ? (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout }) : undefined; + } + async collect(defaults) { + if (!this.readline) + return defaults; + console.log(renderBox('This wizard configures repository workflows, GitHub Variables, GitHub Secrets, AI agents, and operational defaults.\n\nThe setup PAT is an operator credential used only during this command. It is different from the workflow PAT that the bot account uses at runtime.', 'Copilot Setup')); + console.log(color('\n1. Choose the capabilities to install\n', 36)); + for (const [feature, description] of Object.entries(setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS)) { + defaults.features[feature] = await this.askBoolean(description, defaults.features[feature] !== false); + } + console.log(color('\n2. Choose one of the three supported agent runtimes for each task\n', 36)); + for (const task of setup_configuration_policy_1.SETUP_AGENT_TASKS) { + defaults.agents[task].provider = await this.askChoice(`${formatTask(task)} runtime`, [...AGENT_PROVIDERS], defaults.agents[task].provider); + } + const modelProvider = await this.askChoice('Model provider for all tasks', [...MODEL_PROVIDERS], defaults.agents.findings.modelProvider); + const model = await this.askText('Model name for all tasks', defaults.agents.findings.model); + const effort = await this.askText('Reasoning effort for all tasks (leave empty for provider default)', defaults.agents.findings.effort ?? ''); + for (const task of setup_configuration_policy_1.SETUP_AGENT_TASKS) { + defaults.agents[task].modelProvider = modelProvider; + defaults.agents[task].model = model; + defaults.agents[task].effort = effort; + } + if (await this.askBoolean('Configure model provider, model, and effort independently for every task?', false)) { + for (const task of setup_configuration_policy_1.SETUP_AGENT_TASKS) { + defaults.agents[task].modelProvider = await this.askText(`${formatTask(task)} model provider`, defaults.agents[task].modelProvider); + defaults.agents[task].model = await this.askText(`${formatTask(task)} model`, defaults.agents[task].model); + defaults.agents[task].effort = await this.askText(`${formatTask(task)} effort (empty for default)`, defaults.agents[task].effort ?? ''); + } + } + console.log(color('\n3. Configure repository behavior\n', 36)); + const repository = defaults.repository; + repository.mainBranch = await this.askText('Production branch', repository.mainBranch); + repository.developmentBranch = await this.askText('Development branch', repository.developmentBranch); + repository.featureTree = await this.askText('Feature branch prefix', repository.featureTree); + repository.bugfixTree = await this.askText('Bugfix branch prefix', repository.bugfixTree); + repository.hotfixTree = await this.askText('Hotfix branch prefix', repository.hotfixTree); + repository.releaseTree = await this.askText('Release branch prefix', repository.releaseTree); + repository.docsTree = await this.askText('Documentation branch prefix', repository.docsTree); + repository.choreTree = await this.askText('Chore branch prefix', repository.choreTree); + repository.branchManagementAlways = await this.askBoolean('Create/manage branches without requiring the branched label?', repository.branchManagementAlways); + repository.reopenIssueOnPush = await this.askBoolean('Reopen closed issues when a related branch receives a push?', repository.reopenIssueOnPush); + repository.desiredAssigneesCount = await this.askNumber('Desired issue assignees (0 disables automatic assignment)', repository.desiredAssigneesCount); + repository.desiredReviewersCount = await this.askNumber('Desired pull-request reviewers (0 disables automatic assignment)', repository.desiredReviewersCount); + repository.mergeTimeout = await this.askNumber('Merge timeout in seconds (0 disables the timeout)', repository.mergeTimeout); + repository.issueLocale = await this.askText('Issue comment locale', repository.issueLocale); + repository.pullRequestLocale = await this.askText('Pull-request comment locale', repository.pullRequestLocale); + repository.commitPrefixTransforms = await this.askText('Commit prefix transforms', repository.commitPrefixTransforms); + console.log(color('\n4. Configure AI, projects, and release safety\n', 36)); + const ai = defaults.ai; + ai.pullRequestDescription = await this.askBoolean('Generate AI pull-request descriptions?', ai.pullRequestDescription); + ai.ignoreFiles = await this.askText('AI ignore file patterns (comma-separated)', ai.ignoreFiles); + ai.membersOnly = await this.askBoolean('Restrict AI processing to repository members?', ai.membersOnly); + ai.includeReasoning = await this.askBoolean('Include agent reasoning where supported?', ai.includeReasoning); + ai.bugbotSeverity = await this.askChoice('Minimum Bugbot severity to publish', ['info', 'low', 'medium', 'high'], ai.bugbotSeverity); + ai.bugbotCommentLimit = await this.askNumber('Maximum Bugbot comments per run', ai.bugbotCommentLimit); + ai.bugbotFixVerifyCommands = await this.askText('Bugbot autofix verification commands (comma-separated, empty is allowed)', ai.bugbotFixVerifyCommands); + ai.provisioningMode = await this.askChoice('Agent CLI provisioning mode', ['auto', 'always', 'disabled'], ai.provisioningMode); + defaults.projects.ids = await this.askText('GitHub Project IDs (comma-separated, empty to skip Projects integration)', defaults.projects.ids); + if (defaults.projects.ids.trim()) { + defaults.projects.issueCreatedColumn = await this.askText('Project column for new issues', defaults.projects.issueCreatedColumn); + defaults.projects.pullRequestCreatedColumn = await this.askText('Project column for new pull requests', defaults.projects.pullRequestCreatedColumn); + defaults.projects.issueInProgressColumn = await this.askText('Project column for issues in progress', defaults.projects.issueInProgressColumn); + defaults.projects.pullRequestInProgressColumn = await this.askText('Project column for pull requests in progress', defaults.projects.pullRequestInProgressColumn); + } + defaults.createInitialTag = await this.askBoolean('Create v1.0.0 when the repository has no version tags?', defaults.createInitialTag); + defaults.manageRepositoryVariables = await this.askBoolean('Create/update the non-sensitive GitHub Repository Variables used by the workflows?', defaults.manageRepositoryVariables); + defaults.manageRepositorySecrets = await this.askBoolean('Validate and provision the GitHub Secrets required by the selected workflows?', defaults.manageRepositorySecrets); + return defaults; + } + showPlan(plan) { + const enabledFeatures = Object.entries(plan.configuration.features) + .filter(([, enabled]) => enabled) + .map(([feature]) => ` ${color('✓', 32)} ${setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS[feature] ?? feature}`) + .join('\n'); + const agents = setup_configuration_policy_1.SETUP_AGENT_TASKS + .map(task => ` ${formatTask(task)}: ${plan.configuration.agents[task].provider} / ${plan.configuration.agents[task].modelProvider}/${plan.configuration.agents[task].model}`) + .join('\n'); + const content = [ + color('Capabilities', 36), enabledFeatures || ' (none)', '', + color('Agent routing', 36), agents, '', + color('Repository changes', 36), + ` Files selected: ${plan.selectedFiles.length}`, + ` Variables to upsert: ${plan.configuration.manageRepositoryVariables ? plan.variables.length : 0}`, + ` Secrets to validate/provision: ${plan.configuration.manageRepositorySecrets ? plan.credentialRequirements.length : 0}`, + ` Labels and issue types: always checked by Copilot setup`, + ` Initial tag: ${plan.configuration.createInitialTag ? 'v1.0.0 when no version tag exists' : 'disabled'}`, '', + color('Credential contract', 33), ` ${plan.requiredSecrets.join(', ')}`, + ...(plan.warnings.length > 0 ? ['', color('Important notes', 33), ...plan.warnings.map(warning => ` ⚠ ${warning}`)] : []), + ].join('\n'); + console.log(renderBox(content, 'Setup Plan', 32)); + } + async confirm(plan) { + if (this.assumeYes || !this.readline) + return true; + return this.askBoolean(`Apply this setup plan to ${plan.configuration.manageRepositoryVariables ? 'the repository and GitHub Variables' : 'the repository'}?`, false); + } + async requestSetupPat() { + if (!this.readline) + return undefined; + console.log(renderBox('Enter a GitHub setup PAT. It is used in memory for this run only and is never stored in the repository, a .env file, or a GitHub Secret.\n\nRecommended fine-grained permissions for the selected setup features:\n Repository: Metadata read, Contents read, Issues write, Actions read/write, Variables write, Secrets read/write, Workflows read/write.\n Organization: Issue Types write and Projects read/write only when selected; Members read when member-only checks are enabled.\n Contents write and Workflows write are needed only when changing workflow files through the GitHub API.\n\nThe workflow PAT is a different bot-account token and is requested separately.', 'Setup PAT', 33)); + return this.askSecret('Setup PAT'); + } + explainCredentialSeparation(requirements) { + if (!this.readline) + return; + console.log(renderBox('The workflow PAT is not the setup PAT. The workflow PAT belongs to the bot account, is stored remotely as the PAT Secret, and is used by GitHub Actions to work on issues and pull requests. Existing Secrets are never readable through GitHub; Copilot can only validate them through the repository health workflow.', 'Workflow credentials', 33)); + console.log(`Required credentials: ${requirements.map(requirement => requirement.name).join(', ')}`); + } + async requestWorkflowPat(requirement, current) { + return this.requestSecretForRequirement(requirement, current, 'workflow PAT owned by the bot account'); + } + async requestApiKey(requirement, current) { + return this.requestSecretForRequirement(requirement, current, `${requirement.provider ?? 'provider'} API key`); + } + async chooseExistingCredential(requirement, check) { + if (this.credentialValues[requirement.name]?.trim()) + return 'replace'; + if (!this.readline) + return 'keep'; + console.log(`Existing ${requirement.name}: ${check.status}. ${check.message}`); + return this.askChoice(`How should Copilot handle the existing ${requirement.name}?`, ['keep', 'replace', 'skip'], check.status === 'valid' ? 'keep' : 'replace'); + } + showCredentialChecks(checks) { + if (checks.length === 0) + return; + console.log(renderBox(checks.map(check => ` ${statusIcon(check.status)} ${check.name}: ${check.status} — ${check.message}`).join('\n'), 'Credential validation', checks.some(check => check.status === 'invalid') ? 31 : 32)); + } + showDoctorChecks(checks) { + const content = checks.map(check => ` ${doctorIcon(check.status)} ${check.area}: ${check.message}`).join('\n'); + console.log(renderBox(content || ' No checks were available.', 'Copilot Doctor', checks.some(check => check.status === 'fail') ? 31 : 32)); + } + async confirmWorkflowUpdates(comparisons, forcedByFlag) { + const changed = comparisons.filter(comparison => comparison.status === 'changed' || comparison.status === 'unmanaged'); + if (changed.length === 0) + return false; + if (!this.readline) + return forcedByFlag; + console.log(renderBox(changed.map(comparison => ` ${comparison.status === 'changed' ? '↻' : '⚠'} ${comparison.destination} (${comparison.status})`).join('\n'), 'Existing workflows detected', 33)); + if (forcedByFlag) { + console.log('The --update-workflows flag was provided; these setup-managed workflows are eligible for update.'); + return true; + } + return this.askBoolean('Update the detected workflows with the configuration selected in this setup?', false); + } + close() { + this.readline?.close(); + } + async askText(question, defaultValue) { + const answer = await this.readline.question(`${question} ${color(`[${defaultValue || 'none'}]`, 90)}: `); + return answer.trim() || defaultValue; + } + async requestSecretForRequirement(requirement, current, label) { + const supplied = this.credentialValues[requirement.name]?.trim(); + if (supplied) + return { name: requirement.name, value: supplied }; + if (!this.readline) + return undefined; + if (current) { + console.log(`${requirement.name}: ${current.status} (${current.message})`); + } + const value = await this.askSecret(`${requirement.name} — ${label}`); + return value ? { name: requirement.name, value } : undefined; + } + async askSecret(question) { + const input = node_process_1.stdin; + if (!input.isTTY || !input.setRawMode) { + return (await this.readline.question(`${question}: `)).trim(); + } + node_process_1.stdout.write(`${question}: `); + input.setRawMode(true); + input.resume(); + return await new Promise((resolve, reject) => { + let value = ''; + const onData = (chunk) => { + const text = chunk.toString(); + for (const character of text) { + if (character === '\u0003') { + cleanup(); + reject(new Error('Input cancelled.')); + } + else if (character === '\r' || character === '\n') { + cleanup(); + node_process_1.stdout.write('\n'); + resolve(value.trim()); + } + else if (character === '\u007f') { + value = value.slice(0, -1); + } + else { + value += character; + } + } + }; + const cleanup = () => { + input.off('data', onData); + input.setRawMode?.(false); + input.pause(); + }; + input.on('data', onData); + }); + } + async askNumber(question, defaultValue) { + while (true) { + const value = await this.askText(question, String(defaultValue)); + const parsed = Number(value); + if (Number.isInteger(parsed) && parsed >= 0) + return parsed; + console.log(color('Please enter a non-negative whole number.', 33)); + } + } + async askBoolean(question, defaultValue) { + const answer = await this.readline.question(`${question} ${color(`[${defaultValue ? 'Y' : 'N'}]`, 90)}: `); + const normalized = answer.trim().toLowerCase(); + if (!normalized) + return defaultValue; + return ['y', 'yes', 'true'].includes(normalized); + } + async askChoice(question, choices, defaultValue) { + console.log(question); + choices.forEach((choice, index) => console.log(` ${index + 1}) ${choice}${choice === defaultValue ? color(' (default)', 90) : ''}`)); + while (true) { + const answer = await this.readline.question(`Select 1-${choices.length} ${color(`[${choices.indexOf(defaultValue) + 1}]`, 90)}: `); + if (!answer.trim()) + return defaultValue; + const index = Number(answer) - 1; + if (Number.isInteger(index) && choices[index]) + return choices[index]; + console.log(color('Please select one of the listed options.', 33)); + } + } +} +exports.SetupPromptAdapter = SetupPromptAdapter; +function statusIcon(status) { + if (status === 'valid') + return '✓'; + if (status === 'unverifiable') + return '?'; + if (status === 'missing') + return '!'; + if (status === 'not_required') + return '–'; + return '✗'; +} +function doctorIcon(status) { + return status === 'pass' ? '✓' : status === 'warn' ? '⚠' : '✗'; +} +function formatTask(task) { + return task.charAt(0).toUpperCase() + task.slice(1); +} +function color(value, code) { + if (!node_process_1.stdout.isTTY) + return value; + return `\u001b[${code}m${value}\u001b[0m`; +} +function renderBox(content, title, borderCode = 36) { + const lines = [` ${title} `, ...content.split('\n').map(line => ` ${line}`)]; + const width = Math.max(...lines.map(line => stripAnsi(line).length)) + 1; + const border = color(`╭${'─'.repeat(width)}╮`, borderCode); + const bottom = color(`╰${'─'.repeat(width)}╯`, borderCode); + return [ + border, + ...lines.map(line => `${color('│', borderCode)}${line}${' '.repeat(Math.max(0, width - stripAnsi(line).length))}${color('│', borderCode)}`), + bottom, + ].join('\n'); +} +function stripAnsi(value) { + return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''); +} + + /***/ }), /***/ 21307: @@ -67616,7 +70972,7 @@ async function ensureConfiguredIssueTypeSafely(client, owner, configured) { catch (error) { const message = error instanceof Error ? error.message : String(error); (0, logger_1.logError)(`Error ensuring issue type "${configured.name}": ${error}`); - return { kind: 'error', message: `Error creando tipo de Issue "${configured.name}": ${message}` }; + return { kind: 'error', message: `Error creating Issue type "${configured.name}": ${message}` }; } } function ensureConfiguredIssueType(client, owner, configured) { @@ -67661,22 +71017,22 @@ async function listIssueTypes(client, owner) { const response = await client.graphql(ISSUE_TYPES_QUERY, { owner, after: cursor }); const organization = response.organization; if (!organization) - throw new Error(`No se pudo obtener la organización ${owner}`); + throw new Error(`Could not resolve the organization ${owner}`); issueTypes.push(...organization.issueTypes.nodes); const pageInfo = organization.issueTypes.pageInfo; if (!pageInfo?.hasNextPage) return issueTypes; if (!pageInfo.endCursor) { - throw new Error(`La paginación de tipos de Issue no devolvió cursor en la página ${page}.`); + throw new Error(`Issue type pagination did not return a cursor on page ${page}.`); } cursor = pageInfo.endCursor; } - throw new Error("La paginación de tipos de Issue superó 100 páginas."); + throw new Error('Issue type pagination exceeded 100 pages.'); } async function createIssueType(client, owner, name, description, color) { const response = await client.graphql(ORGANIZATION_ID_QUERY, { owner }); if (!response.organization) - throw new Error(`No se pudo obtener la organización ${owner}`); + throw new Error(`Could not resolve the organization ${owner}`); const result = await client.graphql(CREATE_ISSUE_TYPE_MUTATION, { ownerId: response.organization.id, name, @@ -69789,6 +73145,114 @@ function releaseIdAsString(id) { } +/***/ }), + +/***/ 28493: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.RepositoryVariablesRepository = void 0; +exports.encryptSecret = encryptSecret; +const tweetnacl_1 = __importDefault(__nccwpck_require__(24258)); +const node_crypto_1 = __nccwpck_require__(6005); +class RepositoryVariablesRepository { + constructor(githubClient) { + this.githubClient = githubClient; + } + async list(owner, repository, token) { + const client = this.githubClient.getClient(token); + if (!client.rest.secrets) + throw new Error('GitHub repository Secret API is unavailable.'); + const response = await client.rest.secrets.listRepoSecrets({ owner, repo: repository, per_page: 100 }); + return response.data.secrets.map(secret => secret.name); + } + async listVariables(owner, repository, token) { + const client = this.githubClient.getClient(token); + const response = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); + return response.data.variables.map(variable => ({ name: variable.name, value: variable.value })); + } + async upsertSecrets(owner, repository, token, credentials) { + const client = this.githubClient.getClient(token); + if (!client.rest.secrets) + throw new Error('GitHub repository Secret API is unavailable.'); + const existing = new Set(await this.list(owner, repository, token)); + const publicKey = await client.rest.secrets.getRepoPublicKey({ owner, repo: repository }); + let created = 0; + let updated = 0; + const skipped = 0; + const errors = []; + for (const credential of credentials) { + try { + await client.rest.secrets.createOrUpdateRepoSecret({ + owner, + repo: repository, + secret_name: credential.name, + encrypted_value: encryptSecret(credential.value, publicKey.data.key), + key_id: publicKey.data.key_id, + }); + if (existing.has(credential.name)) + updated += 1; + else + created += 1; + } + catch (error) { + errors.push(`Error configuring repository Secret ${credential.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, skipped, errors }; + } + /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ + async upsert(owner, repository, token, variables) { + return this.upsertVariables(owner, repository, token, variables); + } + async upsertVariables(owner, repository, token, variables) { + const client = this.githubClient.getClient(token); + const existing = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); + const existingValues = new Map(existing.data.variables.map(variable => [variable.name, variable.value])); + let created = 0; + let updated = 0; + const errors = []; + for (const variable of variables) { + try { + if (existingValues.has(variable.name)) { + if (existingValues.get(variable.name) === variable.value) + continue; + await client.rest.actions.updateRepoVariable({ owner, repo: repository, name: variable.name, value: variable.value }); + updated += 1; + } + else { + await client.rest.actions.createRepoVariable({ owner, repo: repository, name: variable.name, value: variable.value }); + created += 1; + } + } + catch (error) { + errors.push(`Error configuring repository Variable ${variable.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, errors }; + } +} +exports.RepositoryVariablesRepository = RepositoryVariablesRepository; +/** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ +function encryptSecret(value, base64PublicKey) { + const publicKey = Buffer.from(base64PublicKey, 'base64'); + if (publicKey.length !== tweetnacl_1.default.box.publicKeyLength) + throw new Error('GitHub returned an invalid repository public key.'); + const keyPair = tweetnacl_1.default.box.keyPair(); + const nonce = (0, node_crypto_1.createHash)('blake2b512') + .update(Buffer.concat([Buffer.from(keyPair.publicKey), publicKey])) + .digest() + .subarray(0, tweetnacl_1.default.box.nonceLength); + const ciphertext = tweetnacl_1.default.box(Buffer.from(value, 'utf8'), nonce, publicKey, keyPair.secretKey); + return Buffer.from(Buffer.concat([Buffer.from(keyPair.publicKey), Buffer.from(ciphertext)])).toString('base64'); +} + + /***/ }), /***/ 40941: @@ -70824,14 +74288,17 @@ exports.createBranchComparisonClient = createBranchComparisonClient; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createOrganizationMembersClient = exports.createActorAuthorizationClient = exports.createAuthenticatedUserClient = void 0; +exports.createRepositoryVariablesClient = exports.createOrganizationMembersClient = exports.createActorAuthorizationClient = exports.createAuthenticatedUserClient = void 0; const octokit_identity_adapters_1 = __nccwpck_require__(29996); +const octokit_repository_variables_adapter_1 = __nccwpck_require__(81329); const createAuthenticatedUserClient = () => new octokit_identity_adapters_1.OctokitAuthenticatedUserClientAdapter(); exports.createAuthenticatedUserClient = createAuthenticatedUserClient; const createActorAuthorizationClient = () => new octokit_identity_adapters_1.OctokitActorAuthorizationClientAdapter(); exports.createActorAuthorizationClient = createActorAuthorizationClient; const createOrganizationMembersClient = () => new octokit_identity_adapters_1.OctokitOrganizationMembersClientAdapter(); exports.createOrganizationMembersClient = createOrganizationMembersClient; +const createRepositoryVariablesClient = () => new octokit_repository_variables_adapter_1.OctokitRepositoryVariablesClientAdapter(); +exports.createRepositoryVariablesClient = createRepositoryVariablesClient; /***/ }), @@ -70948,9 +74415,12 @@ const repository_tag_repository_1 = __nccwpck_require__(58717); const git_cli_repository_1 = __nccwpck_require__(26331); const initial_setup_use_case_composition_1 = __nccwpck_require__(93141); const setup_workspace_adapter_1 = __nccwpck_require__(5729); +const repository_variables_repository_1 = __nccwpck_require__(28493); +const github_identity_client_factory_2 = __nccwpck_require__(93081); function createInitialSetupCompositionRoot() { const labelProvisioning = new issue_label_provisioning_repository_1.IssueLabelProvisioningRepository((0, github_issue_client_factory_1.createIssueLabelProvisioningClient)()); - return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter()); + const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_2.createRepositoryVariablesClient)()); + return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter(), repositoryConfiguration, repositoryConfiguration); } @@ -71392,6 +74862,49 @@ function createPullRequestUseCaseCompositionRoot() { } +/***/ }), + +/***/ 69084: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSetupCredentialsUseCase = createSetupCredentialsUseCase; +const setup_credentials_use_case_1 = __nccwpck_require__(67438); +const setup_credential_validation_adapter_1 = __nccwpck_require__(47020); +const repository_variables_repository_1 = __nccwpck_require__(28493); +const github_identity_client_factory_1 = __nccwpck_require__(93081); +const setup_remote_credential_health_adapter_1 = __nccwpck_require__(1489); +const octokit_credential_health_adapter_1 = __nccwpck_require__(41760); +function createSetupCredentialsUseCase(prompt) { + const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_1.createRepositoryVariablesClient)()); + return new setup_credentials_use_case_1.SetupCredentialsUseCase(prompt, new setup_credential_validation_adapter_1.SetupCredentialValidationAdapter(), repositoryConfiguration, new setup_remote_credential_health_adapter_1.SetupRemoteCredentialHealthAdapter(new octokit_credential_health_adapter_1.OctokitCredentialHealthClientAdapter(), { bootstrapWhenMissing: true })); +} + + +/***/ }), + +/***/ 56360: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSetupDoctorUseCase = createSetupDoctorUseCase; +const doctor_use_case_1 = __nccwpck_require__(87328); +const setup_credential_validation_adapter_1 = __nccwpck_require__(47020); +const repository_variables_repository_1 = __nccwpck_require__(28493); +const github_identity_client_factory_1 = __nccwpck_require__(93081); +const setup_workspace_adapter_1 = __nccwpck_require__(5729); +const setup_remote_credential_health_adapter_1 = __nccwpck_require__(1489); +const octokit_credential_health_adapter_1 = __nccwpck_require__(41760); +function createSetupDoctorUseCase(output) { + const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_1.createRepositoryVariablesClient)()); + return new doctor_use_case_1.SetupDoctorUseCase(new setup_credential_validation_adapter_1.SetupCredentialValidationAdapter(), repositoryConfiguration, repositoryConfiguration, new setup_workspace_adapter_1.SetupWorkspaceAdapter(), output, new setup_remote_credential_health_adapter_1.SetupRemoteCredentialHealthAdapter(new octokit_credential_health_adapter_1.OctokitCredentialHealthClientAdapter())); +} + + /***/ }), /***/ 21598: @@ -71561,6 +75074,24 @@ function getOctokitClient(token) { } +/***/ }), + +/***/ 41760: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OctokitCredentialHealthClientAdapter = void 0; +const octokit_client_resolver_1 = __nccwpck_require__(54047); +class OctokitCredentialHealthClientAdapter { + getClient(token) { + return (0, octokit_client_resolver_1.getOctokitClient)(token); + } +} +exports.OctokitCredentialHealthClientAdapter = OctokitCredentialHealthClientAdapter; + + /***/ }), /***/ 29996: @@ -71697,6 +75228,24 @@ class OctokitReleaseClientAdapter { exports.OctokitReleaseClientAdapter = OctokitReleaseClientAdapter; +/***/ }), + +/***/ 81329: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OctokitRepositoryVariablesClientAdapter = void 0; +const octokit_client_resolver_1 = __nccwpck_require__(54047); +class OctokitRepositoryVariablesClientAdapter { + getClient(token) { + return (0, octokit_client_resolver_1.getOctokitClient)(token); + } +} +exports.OctokitRepositoryVariablesClientAdapter = OctokitRepositoryVariablesClientAdapter; + + /***/ }), /***/ 86719: @@ -71798,6 +75347,336 @@ class LoggerWorkflowPollingObserverAdapter { exports.LoggerWorkflowPollingObserverAdapter = LoggerWorkflowPollingObserverAdapter; +/***/ }), + +/***/ 47020: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SetupCredentialValidationAdapter = void 0; +/** + * Performs bounded, metadata-only credential checks. Provider responses are + * intentionally never returned or logged because they can contain account data. + */ +class SetupCredentialValidationAdapter { + constructor(options = {}) { + this.fetcher = options.fetcher ?? fetch; + this.timeoutMs = options.timeoutMs ?? 10000; + } + async validateSetupPat(owner, repository, token) { + try { + const user = await this.requestJson('https://api.github.com/user', { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + }); + const account = typeof user.login === 'string' ? user.login : undefined; + await this.requestJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}`, { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + }); + return { name: 'SETUP_PAT', status: 'valid', message: 'GitHub identity and repository access verified.', account }; + } + catch (error) { + return { name: 'SETUP_PAT', status: classifyError(error), message: safeMessage(error) }; + } + } + async validateCredential(requirement, value) { + const endpoint = endpointFor(requirement); + if (!endpoint) { + return { name: requirement.name, status: 'unverifiable', message: 'This provider does not expose a safe metadata-only validation endpoint.' }; + } + try { + const headers = { Accept: 'application/json' }; + const init = { method: 'GET', headers }; + if (endpoint.auth === 'bearer') + headers.Authorization = `Bearer ${value}`; + if (endpoint.auth === 'x-api-key') + headers['x-api-key'] = value; + if (endpoint.auth === 'query') + endpoint.url.searchParams.set('key', value); + if (endpoint.auth === 'basic') + headers.Authorization = `Basic ${Buffer.from(`${value}:`).toString('base64')}`; + if (requirement.provider === 'anthropic') + headers['anthropic-version'] = '2023-06-01'; + const response = await this.requestJson(endpoint.url.toString(), headers, init); + if (requirement.model && !modelIsAvailable(response, requirement.model, requirement.provider)) { + return { name: requirement.name, status: 'invalid', message: `Credential is valid, but model ${requirement.model} is not available to it.` }; + } + return { name: requirement.name, status: 'valid', message: 'Provider metadata request succeeded.' }; + } + catch (error) { + return { name: requirement.name, status: classifyError(error), message: safeMessage(error) }; + } + finally { + if (endpoint.auth === 'query') + endpoint.url.searchParams.delete('key'); + } + } + async requestJson(url, headers, init = {}) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const response = await this.fetcher(url, { ...init, headers, signal: controller.signal }); + if (!response.ok) + throw new CredentialHttpError(response.status); + const body = await response.json(); + return body && typeof body === 'object' ? body : {}; + } + finally { + clearTimeout(timeout); + } + } +} +exports.SetupCredentialValidationAdapter = SetupCredentialValidationAdapter; +function endpointFor(requirement) { + switch (requirement.name) { + case 'OPENAI_API_KEY': + case 'CODEX_ACCESS_TOKEN': + return { url: new URL('https://api.openai.com/v1/models'), auth: 'bearer' }; + case 'ANTHROPIC_API_KEY': + return { url: new URL('https://api.anthropic.com/v1/models'), auth: 'x-api-key' }; + case 'GOOGLE_API_KEY': + return { url: new URL('https://generativelanguage.googleapis.com/v1beta/models'), auth: 'query' }; + case 'OPENROUTER_API_KEY': + return { url: new URL('https://openrouter.ai/api/v1/models'), auth: 'bearer' }; + case 'CURSOR_API_KEY': + return { url: new URL('https://api.cursor.com/analytics/ai-code/changes?startDate=30d&page=1&pageSize=1'), auth: 'basic' }; + case 'OPENCODE_API_KEY': + return { url: new URL('https://opencode.ai/zen/v1/models'), auth: 'bearer' }; + default: + return undefined; + } +} +function modelIsAvailable(payload, model, provider) { + const data = Array.isArray(payload.data) ? payload.data : Array.isArray(payload.models) ? payload.models : []; + if (data.length === 0) + return true; + const normalized = model.replace(/^models\//, '').toLowerCase(); + return data.some(item => { + if (!item || typeof item !== 'object') + return false; + const candidate = item; + const id = String(candidate.id ?? candidate.name ?? '').replace(/^models\//, '').toLowerCase(); + return id === normalized || (provider === 'google' && id.endsWith(`/${normalized}`)); + }); +} +class CredentialHttpError extends Error { + constructor(status) { + super(`Provider rejected the credential (HTTP ${status}).`); + this.status = status; + } +} +function classifyError(error) { + if (error instanceof CredentialHttpError && (error.status === 401 || error.status === 403)) + return 'invalid'; + if (error instanceof CredentialHttpError && error.status >= 400 && error.status < 500) + return 'invalid'; + return 'unverifiable'; +} +function safeMessage(error) { + if (error instanceof CredentialHttpError) + return error.message; + if (error instanceof DOMException && error.name === 'AbortError') + return 'Validation timed out.'; + return 'Provider validation could not be completed. Check network access and try again.'; +} + + +/***/ }), + +/***/ 1489: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SetupRemoteCredentialHealthAdapter = void 0; +const node_fs_1 = __nccwpck_require__(87561); +const path = __importStar(__nccwpck_require__(49411)); +const WORKFLOW_ID = 'copilot_credential_health.yml'; +const INPUT_BY_SECRET = { + PAT: 'check_pat', + OPENAI_API_KEY: 'check_openai', + ANTHROPIC_API_KEY: 'check_anthropic', + GOOGLE_API_KEY: 'check_google', + OPENROUTER_API_KEY: 'check_openrouter', + CURSOR_API_KEY: 'check_cursor', + OPENCODE_API_KEY: 'check_opencode', + CODEX_ACCESS_TOKEN: 'check_codex', +}; +const JOB_BY_SECRET = { + PAT: 'Verify PAT', + OPENAI_API_KEY: 'Verify OPENAI_API_KEY', + ANTHROPIC_API_KEY: 'Verify ANTHROPIC_API_KEY', + GOOGLE_API_KEY: 'Verify GOOGLE_API_KEY', + OPENROUTER_API_KEY: 'Verify OPENROUTER_API_KEY', + CURSOR_API_KEY: 'Verify CURSOR_API_KEY', + OPENCODE_API_KEY: 'Verify OPENCODE_API_KEY', + CODEX_ACCESS_TOKEN: 'Verify CODEX_ACCESS_TOKEN', +}; +/** Dispatches the repository-owned health workflow; it cannot read or mutate Secret values. */ +class SetupRemoteCredentialHealthAdapter { + constructor(githubClient, options = {}) { + this.githubClient = githubClient; + this.waitMs = options.waitMs ?? 120000; + this.pollMs = options.pollMs ?? 2000; + this.sleep = options.sleep ?? (milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))); + this.bootstrapWhenMissing = options.bootstrapWhenMissing ?? false; + this.workflowContent = options.workflowContent ?? readHealthWorkflow(); + } + async validateExisting(owner, repository, token, ref, requirements) { + const client = this.githubClient.getClient(token); + let temporaryWorkflow = false; + try { + await client.rest.actions.getWorkflow({ owner, repo: repository, workflow_id: WORKFLOW_ID }); + } + catch (error) { + if (isNotFound(error) && this.bootstrapWhenMissing) { + await this.bootstrapWorkflow(client, owner, repository, ref); + temporaryWorkflow = true; + } + else if (isNotFound(error)) + return undefined; + else + throw error; + } + const inputs = {}; + for (const requirement of requirements) { + const input = INPUT_BY_SECRET[requirement.name]; + if (input) + inputs[input] = 'true'; + } + const startedAt = Date.now(); + try { + await client.rest.actions.createWorkflowDispatch({ owner, repo: repository, workflow_id: WORKFLOW_ID, ref, inputs }); + const run = await this.findRun(client, owner, repository, startedAt); + if (!run) + return requirements.map(requirement => ({ name: requirement.name, status: 'unverifiable', message: 'Credential health workflow did not produce a run before timeout.' })); + const jobs = await client.rest.actions.listJobsForWorkflowRun({ owner, repo: repository, run_id: run.id, per_page: 100 }); + const jobsByName = new Map(jobs.data.jobs.map(job => [job.name, job])); + return requirements.map(requirement => ({ + name: requirement.name, + status: healthStatus(requirement, jobsByName), + message: healthMessage(requirement, jobsByName), + })); + } + finally { + if (temporaryWorkflow) + await this.removeTemporaryWorkflow(client, owner, repository, ref); + } + } + async bootstrapWorkflow(client, owner, repository, ref) { + if (!this.workflowContent) + throw new Error('Credential health workflow template is unavailable.'); + await client.repos.createOrUpdateFileContents({ + owner, + repo: repository, + path: `.github/workflows/${WORKFLOW_ID}`, + message: 'chore: temporarily validate Copilot credentials', + content: Buffer.from(this.workflowContent, 'utf8').toString('base64'), + branch: ref, + }); + } + async removeTemporaryWorkflow(client, owner, repository, ref) { + const content = await client.repos.getContent({ owner, repo: repository, path: `.github/workflows/${WORKFLOW_ID}`, ref }); + if (!content.data.sha) + throw new Error('Could not resolve the temporary health workflow revision for cleanup.'); + await client.repos.deleteFile({ + owner, + repo: repository, + path: `.github/workflows/${WORKFLOW_ID}`, + message: 'chore: remove temporary Copilot credential health workflow', + sha: content.data.sha, + branch: ref, + }); + } + async findRun(client, owner, repository, startedAt) { + const deadline = Date.now() + this.waitMs; + while (Date.now() <= deadline) { + const response = await client.rest.actions.listWorkflowRuns({ owner, repo: repository, workflow_id: WORKFLOW_ID, event: 'workflow_dispatch', per_page: 10 }); + const run = response.data.workflow_runs.find(candidate => !candidate.created_at || new Date(candidate.created_at).getTime() >= startedAt - 5000); + if (run) { + while (run.status && run.status !== 'completed' && Date.now() <= deadline) { + await this.sleep(this.pollMs); + const latest = await client.rest.actions.getWorkflowRun({ owner, repo: repository, run_id: run.id }); + Object.assign(run, latest.data); + } + return run; + } + await this.sleep(this.pollMs); + } + return undefined; + } +} +exports.SetupRemoteCredentialHealthAdapter = SetupRemoteCredentialHealthAdapter; +function healthStatus(requirement, jobs) { + if (!INPUT_BY_SECRET[requirement.name]) + return 'unverifiable'; + const job = jobs.get(JOB_BY_SECRET[requirement.name]); + if (!job) + return 'unverifiable'; + return job.conclusion === 'success' ? 'valid' : job.conclusion ? 'invalid' : 'unverifiable'; +} +function healthMessage(requirement, jobs) { + if (!INPUT_BY_SECRET[requirement.name]) + return 'No remote health check is implemented for this provider.'; + const job = jobs.get(JOB_BY_SECRET[requirement.name]); + if (!job) + return 'Remote credential health workflow did not report this credential separately.'; + return job.conclusion === 'success' + ? 'Remote credential health check passed.' + : job.conclusion + ? `Remote credential health check failed (${job.conclusion}).` + : 'Remote credential health check is still incomplete.'; +} +function isNotFound(error) { + return Boolean(error && typeof error === 'object' && 'status' in error && error.status === 404); +} +function readHealthWorkflow() { + try { + return (0, node_fs_1.readFileSync)(path.join(__dirname, '..', '..', 'setup', 'workflows', WORKFLOW_ID), 'utf8'); + } + catch { + return ''; + } +} + + /***/ }), /***/ 5729: @@ -71809,13 +75688,23 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupWorkspaceAdapter = void 0; const setup_files_1 = __nccwpck_require__(59126); class SetupWorkspaceAdapter { - prepare() { + prepare(selection) { const workspace = process.cwd(); (0, setup_files_1.ensureGitHubDirs)(workspace); - return (0, setup_files_1.copySetupFiles)(workspace); + if (!selection) + return (0, setup_files_1.copySetupFiles)(workspace); + return (0, setup_files_1.copySetupFiles)(workspace, undefined, selection?.features, { + updateExistingWorkflows: selection?.updateExistingWorkflows, + approvedWorkflowFiles: selection?.approvedWorkflowFiles, + }); + } + hasValidToken(tokenOverride) { + return tokenOverride === undefined + ? (0, setup_files_1.hasValidSetupToken)(process.cwd()) + : (0, setup_files_1.hasValidSetupToken)(process.cwd(), tokenOverride); } - hasValidToken() { - return (0, setup_files_1.hasValidSetupToken)(process.cwd()); + compareWorkflows(features) { + return (0, setup_files_1.compareSetupWorkflows)(process.cwd(), features); } } exports.SetupWorkspaceAdapter = SetupWorkspaceAdapter; @@ -73522,24 +77411,28 @@ exports.copySetupDirectory = copySetupDirectory; const fs = __importStar(__nccwpck_require__(57147)); const path = __importStar(__nccwpck_require__(71017)); const logger_1 = __nccwpck_require__(91151); -function copySetupFile(source, destination, displaySource, displayDestination) { +function copySetupFile(source, destination, displaySource, displayDestination, options = {}) { if (!fs.existsSync(source)) return { copied: 0, skipped: 0 }; - if (fs.existsSync(destination)) { + if (fs.existsSync(destination) && !options.overwrite) { (0, logger_1.logInfo)(` ⏭️ ${displayDestination} already exists; skipping.`); return { copied: 0, skipped: 1 }; } + if (fs.existsSync(destination) && options.backupDirectory) { + fs.mkdirSync(options.backupDirectory, { recursive: true }); + fs.copyFileSync(destination, path.join(options.backupDirectory, path.basename(destination))); + } fs.copyFileSync(source, destination); - (0, logger_1.logInfo)(` ✅ Copied ${displaySource} → ${displayDestination}`); + (0, logger_1.logInfo)(` ${options.overwrite ? '↻ Updated' : '✅ Copied'} ${displaySource} → ${displayDestination}`); return { copied: 1, skipped: 0 }; } -function copySetupDirectory(sourceDirectory, destinationDirectory, fileFilter, displayDirectory) { +function copySetupDirectory(sourceDirectory, destinationDirectory, fileFilter, displayDirectory, options = {}) { if (!fs.existsSync(sourceDirectory)) return { copied: 0, skipped: 0 }; return fs.readdirSync(sourceDirectory) .filter(fileFilter) .filter((fileName) => fs.statSync(path.join(sourceDirectory, fileName)).isFile()) - .map((fileName) => copySetupFile(path.join(sourceDirectory, fileName), path.join(destinationDirectory, fileName), `${displayDirectory}/${fileName}`, `${displayDirectory.replace('setup/', '.github/')}/${fileName}`)) + .map((fileName) => copySetupFile(path.join(sourceDirectory, fileName), path.join(destinationDirectory, fileName), `${displayDirectory}/${fileName}`, `${displayDirectory.replace('setup/', '.github/')}/${fileName}`, options)) .reduce((total, current) => ({ copied: total.copied + current.copied, skipped: total.skipped + current.skipped, @@ -73590,10 +77483,9 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ensureGitHubDirs = ensureGitHubDirs; exports.copySetupFiles = copySetupFiles; -exports.ensureEnvWithToken = ensureEnvWithToken; +exports.compareSetupWorkflows = compareSetupWorkflows; exports.getSetupToken = getSetupToken; exports.hasValidSetupToken = hasValidSetupToken; -exports.setupEnvFileExists = setupEnvFileExists; const fs = __importStar(__nccwpck_require__(57147)); const path = __importStar(__nccwpck_require__(71017)); const setup_file_copy_1 = __nccwpck_require__(90102); @@ -73628,57 +77520,74 @@ function ensureGitHubDirs(cwd) { * @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root. * @returns { copied, skipped } */ -function copySetupFiles(cwd, setupDirOverride) { +function copySetupFiles(cwd, setupDirOverride, features, options = {}) { const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup'); if (!fs.existsSync(setupDir)) return { copied: 0, skipped: 0 }; - const workflows = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'workflows'), path.join(cwd, '.github', 'workflows'), (fileName) => fileName.endsWith('.yml') || fileName.endsWith('.yaml'), 'setup/workflows'); - const issueTemplates = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'ISSUE_TEMPLATE'), path.join(cwd, '.github', 'ISSUE_TEMPLATE'), () => true, 'setup/ISSUE_TEMPLATE'); - const pullRequestTemplate = (0, setup_file_copy_1.copySetupFile)(path.join(setupDir, 'pull_request_template.md'), path.join(cwd, '.github', 'pull_request_template.md'), 'setup/pull_request_template.md', '.github/pull_request_template.md'); - // Credentials are deliberately never copied from the package. Keep the - // destination check here so setup can guide users to their local .env. - ensureEnvWithToken(cwd); + const workflowFeatures = { + 'copilot_issue.yml': 'issues', + 'copilot_pull_request.yml': 'pullRequests', + 'copilot_commit.yml': 'commits', + 'copilot_issue_comment.yml': 'issueComments', + 'copilot_pull_request_comment.yml': 'pullRequestComments', + 'release_workflow.yml': 'release', + 'hotfix_workflow.yml': 'hotfix', + 'agent-cli-provisioning.yml': 'agentProvisioning', + 'copilot_credential_health.yml': 'credentialHealth', + }; + const approvedWorkflowFiles = new Set(options.approvedWorkflowFiles ?? []); + const backupDirectory = options.updateExistingWorkflows ? path.join(cwd, '.copilot', 'setup-backups', new Date().toISOString().replace(/[:.]/g, '-')) : undefined; + const workflows = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'workflows'), path.join(cwd, '.github', 'workflows'), (fileName) => (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) + && (features === undefined || features[workflowFeatures[fileName]] !== false) + && (!options.updateExistingWorkflows + || approvedWorkflowFiles.has(fileName) + || !fs.existsSync(path.join(cwd, '.github', 'workflows', fileName))), 'setup/workflows', { + overwrite: options.updateExistingWorkflows, + backupDirectory, + }); + const issueTemplates = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'ISSUE_TEMPLATE'), path.join(cwd, '.github', 'ISSUE_TEMPLATE'), (fileName) => features?.issueTemplates !== false + && (features?.release !== false || fileName !== 'release.yml') + && (features?.hotfix !== false || fileName !== 'hotfix.yml'), 'setup/ISSUE_TEMPLATE'); + const pullRequestTemplate = features?.pullRequestTemplate === false + ? { copied: 0, skipped: 0 } + : (0, setup_file_copy_1.copySetupFile)(path.join(setupDir, 'pull_request_template.md'), path.join(cwd, '.github', 'pull_request_template.md'), 'setup/pull_request_template.md', '.github/pull_request_template.md'); return [workflows, issueTemplates, pullRequestTemplate].reduce((total, current) => ({ copied: total.copied + current.copied, skipped: total.skipped + current.skipped, }), { copied: 0, skipped: 0 }); } +function compareSetupWorkflows(cwd, features, setupDirOverride) { + const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup'); + const workflowFeatures = { + 'copilot_issue.yml': 'issues', + 'copilot_pull_request.yml': 'pullRequests', + 'copilot_commit.yml': 'commits', + 'copilot_issue_comment.yml': 'issueComments', + 'copilot_pull_request_comment.yml': 'pullRequestComments', + 'release_workflow.yml': 'release', + 'hotfix_workflow.yml': 'hotfix', + 'agent-cli-provisioning.yml': 'agentProvisioning', + 'copilot_credential_health.yml': 'credentialHealth', + }; + const sourceDirectory = path.join(setupDir, 'workflows'); + if (!fs.existsSync(sourceDirectory)) + return []; + return fs.readdirSync(sourceDirectory) + .filter(file => (file.endsWith('.yml') || file.endsWith('.yaml')) && (features === undefined || features[workflowFeatures[file]] !== false)) + .filter(file => fs.statSync(path.join(sourceDirectory, file)).isFile()) + .map(file => { + const source = path.join(sourceDirectory, file); + const destination = path.join(cwd, '.github', 'workflows', file); + if (!fs.existsSync(destination)) + return { file, destination: `.github/workflows/${file}`, status: 'missing' }; + const equal = fs.readFileSync(source, 'utf8') === fs.readFileSync(destination, 'utf8'); + return { file, destination: `.github/workflows/${file}`, status: equal ? 'unchanged' : 'changed' }; + }); +} const ENV_TOKEN_KEY = 'PERSONAL_ACCESS_TOKEN'; const ENV_PLACEHOLDER_VALUE = 'github_pat_11..'; /** Minimum length for a token to be considered "defined" (not placeholder). */ const MIN_VALID_TOKEN_LENGTH = 20; -function getTokenFromEnvFile(envPath) { - if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) - return null; - const content = fs.readFileSync(envPath, 'utf8'); - const match = content.match(new RegExp(`^${ENV_TOKEN_KEY}=(.+)$`, 'm')); - if (!match) - return null; - const value = match[1].trim().replace(/^["']|["']$/g, ''); - return value.length > 0 ? value : null; -} -/** - * Logs the current state of PERSONAL_ACCESS_TOKEN (environment or .env). Does not create .env. - */ -function ensureEnvWithToken(cwd) { - const envPath = path.join(cwd, '.env'); - const tokenInEnv = process.env[ENV_TOKEN_KEY]?.trim(); - if (tokenInEnv) { - (0, logger_1.logInfo)(' 🔑 PERSONAL_ACCESS_TOKEN is set in environment; .env not needed.'); - return; - } - if (fs.existsSync(envPath)) { - const tokenInFile = getTokenFromEnvFile(envPath); - if (tokenInFile) { - (0, logger_1.logInfo)(' ✅ .env exists and contains PERSONAL_ACCESS_TOKEN.'); - } - else { - (0, logger_1.logInfo)(' ⚠️ .env exists but PERSONAL_ACCESS_TOKEN is missing or empty.'); - } - return; - } - (0, logger_1.logInfo)(' 💡 You can create a .env file here with PERSONAL_ACCESS_TOKEN=your_token or set it in your environment.'); -} function isTokenValueValid(token) { const t = token.trim(); return t.length >= MIN_VALID_TOKEN_LENGTH && t !== ENV_PLACEHOLDER_VALUE; @@ -73686,21 +77595,16 @@ function isTokenValueValid(token) { /** * Resolves the PERSONAL_ACCESS_TOKEN for setup from a single priority order: * 1. override (e.g. CLI --token) if provided and valid, - * 2. process.env.PERSONAL_ACCESS_TOKEN, - * 3. .env file in cwd. + * 2. process.env.PERSONAL_ACCESS_TOKEN. * Returns undefined if no valid token is found. */ -function getSetupToken(cwd, override) { +function getSetupToken(_cwd, override) { const overrideTrimmed = override?.trim(); if (overrideTrimmed && isTokenValueValid(overrideTrimmed)) return overrideTrimmed; const fromEnv = process.env[ENV_TOKEN_KEY]?.trim(); if (fromEnv && isTokenValueValid(fromEnv)) return fromEnv; - const envPath = path.join(cwd, '.env'); - const fromFile = getTokenFromEnvFile(envPath); - if (fromFile !== null && isTokenValueValid(fromFile)) - return fromFile; return undefined; } /** @@ -73710,11 +77614,6 @@ function getSetupToken(cwd, override) { function hasValidSetupToken(cwd, override) { return getSetupToken(cwd, override) !== undefined; } -/** Returns true if a .env file exists in the given directory. */ -function setupEnvFileExists(cwd) { - const envPath = path.join(cwd, '.env'); - return fs.existsSync(envPath) && fs.statSync(envPath).isFile(); -} /***/ }), @@ -74099,6 +77998,14 @@ module.exports = require("node:querystring"); /***/ }), +/***/ 32887: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:readline/promises"); + +/***/ }), + /***/ 84492: /***/ ((module) => { @@ -79926,14 +83833,6 @@ const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0}); "use strict"; module.exports = JSON.parse('{"single":{"topLeft":"┌","top":"─","topRight":"┐","right":"│","bottomRight":"┘","bottom":"─","bottomLeft":"└","left":"│"},"double":{"topLeft":"╔","top":"═","topRight":"╗","right":"║","bottomRight":"╝","bottom":"═","bottomLeft":"╚","left":"║"},"round":{"topLeft":"╭","top":"─","topRight":"╮","right":"│","bottomRight":"╯","bottom":"─","bottomLeft":"╰","left":"│"},"bold":{"topLeft":"┏","top":"━","topRight":"┓","right":"┃","bottomRight":"┛","bottom":"━","bottomLeft":"┗","left":"┃"},"singleDouble":{"topLeft":"╓","top":"─","topRight":"╖","right":"║","bottomRight":"╜","bottom":"─","bottomLeft":"╙","left":"║"},"doubleSingle":{"topLeft":"╒","top":"═","topRight":"╕","right":"│","bottomRight":"╛","bottom":"═","bottomLeft":"╘","left":"│"},"classic":{"topLeft":"+","top":"-","topRight":"+","right":"|","bottomRight":"+","bottom":"-","bottomLeft":"+","left":"|"},"arrow":{"topLeft":"↘","top":"↓","topRight":"↙","right":"←","bottomRight":"↖","bottom":"↑","bottomLeft":"↗","left":"→"}}'); -/***/ }), - -/***/ 92655: -/***/ ((module) => { - -"use strict"; -module.exports = JSON.parse('{"name":"dotenv","version":"16.6.1","description":"Loads environment variables from .env file","main":"lib/main.js","types":"lib/main.d.ts","exports":{".":{"types":"./lib/main.d.ts","require":"./lib/main.js","default":"./lib/main.js"},"./config":"./config.js","./config.js":"./config.js","./lib/env-options":"./lib/env-options.js","./lib/env-options.js":"./lib/env-options.js","./lib/cli-options":"./lib/cli-options.js","./lib/cli-options.js":"./lib/cli-options.js","./package.json":"./package.json"},"scripts":{"dts-check":"tsc --project tests/types/tsconfig.json","lint":"standard","pretest":"npm run lint && npm run dts-check","test":"tap run --allow-empty-coverage --disable-coverage --timeout=60000","test:coverage":"tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov","prerelease":"npm test","release":"standard-version"},"repository":{"type":"git","url":"git://github.com/motdotla/dotenv.git"},"homepage":"https://github.com/motdotla/dotenv#readme","funding":"https://dotenvx.com","keywords":["dotenv","env",".env","environment","variables","config","settings"],"readmeFilename":"README.md","license":"BSD-2-Clause","devDependencies":{"@types/node":"^18.11.3","decache":"^4.6.2","sinon":"^14.0.1","standard":"^17.0.0","standard-version":"^9.5.0","tap":"^19.2.0","typescript":"^4.8.4"},"engines":{"node":">=12"},"browser":{"fs":false}}'); - /***/ }) /******/ }); diff --git a/build/cli/src/application/policies/setup_configuration_policy.d.ts b/build/cli/src/application/policies/setup_configuration_policy.d.ts new file mode 100644 index 00000000..ade1bcf4 --- /dev/null +++ b/build/cli/src/application/policies/setup_configuration_policy.d.ts @@ -0,0 +1,23 @@ +import type { AgentTask } from '../../domain/agent'; +import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement } from '../../domain/setup'; +export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; +export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; +export declare function createDefaultSetupConfiguration(): SetupConfiguration; +export type SetupConfigurationOverrides = { + features?: Partial; + agents?: Partial>>; + repository?: Partial; + ai?: Partial; + projects?: Partial; + createInitialTag?: boolean; + manageRepositoryVariables?: boolean; + manageRepositorySecrets?: boolean; + actionInputs?: Record; +}; +export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; +export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; +export declare function buildSetupPlan(configuration: SetupConfiguration): SetupPlan; +/** Builds the non-sensitive credential contract implied by the selected agents. */ +export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; +export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; +export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; diff --git a/build/cli/src/application/ports/setup_wizard_ports.d.ts b/build/cli/src/application/ports/setup_wizard_ports.d.ts new file mode 100644 index 00000000..3ede0673 --- /dev/null +++ b/build/cli/src/application/ports/setup_wizard_ports.d.ts @@ -0,0 +1,53 @@ +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, DoctorCheck } from '../../domain/setup'; +export interface SetupPromptPort { + collect(defaults: SetupConfiguration): Promise; + showPlan(plan: SetupPlan): void; + confirm(plan: SetupPlan): Promise; + close(): void; +} +export interface SetupCredentialPromptPort { + requestSetupPat(): Promise; + explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; + requestWorkflowPat(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + requestApiKey(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + chooseExistingCredential(requirement: SetupCredentialRequirement, check: SetupCredentialCheck): Promise; + showCredentialChecks(checks: readonly SetupCredentialCheck[]): void; +} +export interface SetupRepositorySecretsPort { + list(owner: string, repository: string, token: string): Promise; + upsertSecrets(owner: string, repository: string, token: string, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; +} +export interface SetupRepositoryConfigurationReadPort { + listVariables(owner: string, repository: string, token: string): Promise; +} +export interface DoctorOutputPort { + showDoctorChecks(checks: readonly DoctorCheck[]): void; +} +export interface SetupCredentialValidationPort { + validateSetupPat(owner: string, repository: string, token: string): Promise; + validateCredential(requirement: SetupCredentialRequirement, value: string): Promise; +} +export interface SetupRemoteCredentialHealthPort { + validateExisting(owner: string, repository: string, token: string, ref: string, requirements: readonly SetupCredentialRequirement[]): Promise; +} +export interface SetupWorkflowUpdatePromptPort { + confirmWorkflowUpdates(comparisons: readonly SetupWorkflowComparison[], forcedByFlag: boolean): Promise; +} +export interface SetupRepositoryVariablesPort { + upsert(owner: string, repository: string, token: string, variables: readonly { + name: string; + value: string; + }[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; +} diff --git a/build/cli/src/application/ports/setup_workspace_ports.d.ts b/build/cli/src/application/ports/setup_workspace_ports.d.ts index 822fa1ff..6588c092 100644 --- a/build/cli/src/application/ports/setup_workspace_ports.d.ts +++ b/build/cli/src/application/ports/setup_workspace_ports.d.ts @@ -1,8 +1,15 @@ +import type { SetupFeatures, SetupWorkflowComparison } from '../../domain/setup'; export interface SetupWorkspaceResult { copied: number; skipped: number; } +export interface SetupWorkspaceSelection { + features?: SetupFeatures; + updateExistingWorkflows?: boolean; + approvedWorkflowFiles?: readonly string[]; +} export interface SetupWorkspacePort { - prepare(): SetupWorkspaceResult; - hasValidToken(): boolean; + prepare(selection?: SetupWorkspaceSelection): SetupWorkspaceResult; + hasValidToken(tokenOverride?: string): boolean; + compareWorkflows?(features?: SetupFeatures): readonly SetupWorkflowComparison[]; } diff --git a/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts b/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts index 2c5cab5a..aab353e2 100644 --- a/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts +++ b/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts @@ -6,6 +6,7 @@ import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { ParamUseCase } from '../base/param_usecase'; +import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; /** Application boundary for provisioning a repository for Copilot automation. */ export declare class InitialSetupUseCase implements ParamUseCase { private readonly authenticatedUserPort; @@ -15,7 +16,9 @@ export declare class InitialSetupUseCase implements ParamUseCase; } diff --git a/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts b/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts index 64ff3bd0..5bc88b49 100644 --- a/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts +++ b/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts @@ -5,6 +5,7 @@ import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports/repository_release_ports'; import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; +import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; export interface InitialSetupWorkflowDependencies { authenticatedUserPort: AuthenticatedUserPort; initialLabelProvisioningPort: InitialLabelProvisioningPort; @@ -13,6 +14,8 @@ export interface InitialSetupWorkflowDependencies { repositoryDefaultBranchPort: RepositoryDefaultBranchPort; repositoryTagPort: RepositoryTagPort; setupWorkspacePort: SetupWorkspacePort; + setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; + setupRepositorySecretsPort?: SetupRepositorySecretsPort; } /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ export declare function runInitialSetupWorkflow(param: Execution, dependencies: InitialSetupWorkflowDependencies): Promise; diff --git a/build/cli/src/application/usecases/setup/doctor_use_case.d.ts b/build/cli/src/application/usecases/setup/doctor_use_case.d.ts new file mode 100644 index 00000000..16692f6f --- /dev/null +++ b/build/cli/src/application/usecases/setup/doctor_use_case.d.ts @@ -0,0 +1,19 @@ +import type { SetupConfiguration } from '../../../domain/setup'; +import type { DoctorOutputPort, SetupCredentialValidationPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; +export interface DoctorRequest { + owner: string; + repository: string; + setupToken: string; + configuration: SetupConfiguration; +} +export declare class SetupDoctorUseCase { + private readonly validation; + private readonly secrets; + private readonly variables; + private readonly workspace; + private readonly output; + private readonly remoteHealth?; + constructor(validation: SetupCredentialValidationPort, secrets: SetupRepositorySecretsPort, variables: SetupRepositoryConfigurationReadPort, workspace: SetupWorkspacePort, output: DoctorOutputPort, remoteHealth?: SetupRemoteCredentialHealthPort | undefined); + execute(request: DoctorRequest): Promise; +} diff --git a/build/cli/src/application/usecases/setup/index.d.ts b/build/cli/src/application/usecases/setup/index.d.ts new file mode 100644 index 00000000..81102e29 --- /dev/null +++ b/build/cli/src/application/usecases/setup/index.d.ts @@ -0,0 +1,4 @@ +export { SetupWizardUseCase } from './setup_wizard_use_case'; +export type { SetupWizardRequest } from './setup_wizard_use_case'; +export { SetupCredentialsUseCase } from './setup_credentials_use_case'; +export type { SetupCredentialsRequest, SetupCredentialsResult } from './setup_credentials_use_case'; diff --git a/build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts b/build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts new file mode 100644 index 00000000..a6a3295f --- /dev/null +++ b/build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts @@ -0,0 +1,24 @@ +import type { SetupCredentialCheck, SetupCredentialCollection, SetupCredentialRequirement } from '../../../domain/setup'; +import type { SetupCredentialPromptPort, SetupCredentialValidationPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +export interface SetupCredentialsRequest { + owner: string; + repository: string; + setupToken: string; + requirements: readonly SetupCredentialRequirement[]; + manageSecrets: boolean; + ref?: string; +} +export interface SetupCredentialsResult { + collection: SetupCredentialCollection; + checks: SetupCredentialCheck[]; + existingSecretNames: readonly string[]; +} +/** Coordinates secret collection and validation without placing secret values in config files. */ +export declare class SetupCredentialsUseCase { + private readonly prompt; + private readonly validation; + private readonly secrets?; + private readonly remoteHealth?; + constructor(prompt: SetupCredentialPromptPort, validation: SetupCredentialValidationPort, secrets?: SetupRepositorySecretsPort | undefined, remoteHealth?: SetupRemoteCredentialHealthPort | undefined); + collect(request: SetupCredentialsRequest): Promise; +} diff --git a/build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts b/build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts new file mode 100644 index 00000000..ad5dfab9 --- /dev/null +++ b/build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts @@ -0,0 +1,14 @@ +import type { SetupPromptPort } from '../../ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan } from '../../../domain/setup'; +import { type SetupConfigurationOverrides } from '../../policies/setup_configuration_policy'; +export interface SetupWizardRequest { + overrides?: SetupConfigurationOverrides; + skipRepositoryVariables?: boolean; +} +export declare class SetupWizardUseCase { + private readonly prompt; + constructor(prompt: SetupPromptPort); + collect(request?: SetupWizardRequest): Promise; + plan(configuration: SetupConfiguration): SetupPlan; + close(): void; +} diff --git a/build/cli/src/cli/commands/doctor.d.ts b/build/cli/src/cli/commands/doctor.d.ts new file mode 100644 index 00000000..a10e18a5 --- /dev/null +++ b/build/cli/src/cli/commands/doctor.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function registerDoctorCommand(program: Command): void; diff --git a/build/cli/src/cli/commands/setup_policy.d.ts b/build/cli/src/cli/commands/setup_policy.d.ts index 8e359df8..670911a0 100644 --- a/build/cli/src/cli/commands/setup_policy.d.ts +++ b/build/cli/src/cli/commands/setup_policy.d.ts @@ -1,5 +1,6 @@ import type { GitInfo } from '../../cli_context'; +import type { SetupConfiguration, SetupCredentialCollection } from '../../domain/setup'; export interface SetupCommandOptions { debug?: boolean; } -export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string): Record | undefined; +export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string, configuration?: SetupConfiguration, credentials?: SetupCredentialCollection, approvedWorkflowFiles?: readonly string[]): Record | undefined; diff --git a/build/cli/src/cli/setup_config_file.d.ts b/build/cli/src/cli/setup_config_file.d.ts new file mode 100644 index 00000000..667ccae4 --- /dev/null +++ b/build/cli/src/cli/setup_config_file.d.ts @@ -0,0 +1,3 @@ +import { type SetupConfigurationOverrides } from '../application/policies/setup_configuration_policy'; +/** Loads a non-secret setup override file. JSON and YAML are supported. */ +export declare function loadSetupConfigurationOverrides(filePath: string): SetupConfigurationOverrides; diff --git a/build/cli/src/cli/setup_prompt_adapter.d.ts b/build/cli/src/cli/setup_prompt_adapter.d.ts new file mode 100644 index 00000000..1ad2dd3d --- /dev/null +++ b/build/cli/src/cli/setup_prompt_adapter.d.ts @@ -0,0 +1,32 @@ +import type { SetupCredentialPromptPort, SetupPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort } from '../application/ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison } from '../domain/setup'; +export interface SetupPromptAdapterOptions { + interactive?: boolean; + assumeYes?: boolean; + credentialValues?: Record; +} +export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { + private readonly interactive; + private readonly assumeYes; + private readonly readline; + private readonly credentialValues; + constructor(options?: SetupPromptAdapterOptions); + collect(defaults: SetupConfiguration): Promise; + showPlan(plan: SetupPlan): void; + confirm(plan: SetupPlan): Promise; + requestSetupPat(): Promise; + explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; + requestWorkflowPat(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + requestApiKey(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + chooseExistingCredential(requirement: SetupCredentialRequirement, check: SetupCredentialCheck): Promise; + showCredentialChecks(checks: readonly SetupCredentialCheck[]): void; + showDoctorChecks(checks: readonly import('../domain/setup').DoctorCheck[]): void; + confirmWorkflowUpdates(comparisons: readonly SetupWorkflowComparison[], forcedByFlag: boolean): Promise; + close(): void; + private askText; + private requestSecretForRequirement; + private askSecret; + private askNumber; + private askBoolean; + private askChoice; +} diff --git a/build/cli/src/data/repository/repository_variables_repository.d.ts b/build/cli/src/data/repository/repository_variables_repository.d.ts new file mode 100644 index 00000000..bd7ceeb5 --- /dev/null +++ b/build/cli/src/data/repository/repository_variables_repository.d.ts @@ -0,0 +1,31 @@ +import type { SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialValue } from '../../domain/setup'; +import type { GithubClientPort } from '../../infrastructure/github/ports/github_client_provider_port'; +import type { GithubRepositoryVariablesClient } from '../../infrastructure/github/ports/github_repository_variables_protocol'; +export declare class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort { + private readonly githubClient; + constructor(githubClient: GithubClientPort); + list(owner: string, repository: string, token: string): Promise; + listVariables(owner: string, repository: string, token: string): Promise; + upsertSecrets(owner: string, repository: string, token: string, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; + /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ + upsert(owner: string, repository: string, token: string, variables: readonly { + name: string; + value: string; + }[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; + private upsertVariables; +} +/** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ +export declare function encryptSecret(value: string, base64PublicKey: string): string; diff --git a/build/cli/src/domain/setup.d.ts b/build/cli/src/domain/setup.d.ts new file mode 100644 index 00000000..e02a2dcf --- /dev/null +++ b/build/cli/src/domain/setup.d.ts @@ -0,0 +1,110 @@ +import type { AgentProvider, AgentTask } from './agent'; +export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'issueTemplates' | 'pullRequestTemplate'; +export interface SetupFeatures { + [feature: string]: boolean; +} +export interface SetupAgentRoleConfiguration { + provider: AgentProvider; + modelProvider: string; + model: string; + effort?: string; +} +export type SetupAgentConfiguration = Record; +export interface SetupRepositoryConfiguration { + mainBranch: string; + developmentBranch: string; + featureTree: string; + bugfixTree: string; + hotfixTree: string; + releaseTree: string; + docsTree: string; + choreTree: string; + branchManagementAlways: boolean; + reopenIssueOnPush: boolean; + desiredAssigneesCount: number; + desiredReviewersCount: number; + mergeTimeout: number; + issueLocale: string; + pullRequestLocale: string; + commitPrefixTransforms: string; +} +export interface SetupAiConfiguration { + pullRequestDescription: boolean; + ignoreFiles: string; + membersOnly: boolean; + includeReasoning: boolean; + bugbotSeverity: 'info' | 'low' | 'medium' | 'high'; + bugbotCommentLimit: number; + bugbotFixVerifyCommands: string; + provisioningMode: 'auto' | 'always' | 'disabled'; +} +export interface SetupProjectConfiguration { + ids: string; + issueCreatedColumn: string; + pullRequestCreatedColumn: string; + issueInProgressColumn: string; + pullRequestInProgressColumn: string; +} +export interface SetupConfiguration { + features: SetupFeatures; + agents: SetupAgentConfiguration; + repository: SetupRepositoryConfiguration; + ai: SetupAiConfiguration; + projects: SetupProjectConfiguration; + createInitialTag: boolean; + manageRepositoryVariables: boolean; + /** Whether setup should provision repository secrets after validating them. */ + manageRepositorySecrets: boolean; + /** Extra non-secret action inputs accepted by config files for advanced use cases. */ + actionInputs: Record; +} +export type SetupCredentialKind = 'workflowPat' | 'apiKey'; +export type SetupCredentialStatus = 'valid' | 'invalid' | 'missing' | 'unverifiable' | 'not_required'; +/** A credential requirement is metadata only; never put a secret value in this object. */ +export interface SetupCredentialRequirement { + name: string; + kind: SetupCredentialKind; + description: string; + provider?: string; + model?: string; +} +export interface SetupCredentialCheck { + name: string; + status: SetupCredentialStatus; + message: string; + account?: string; +} +export interface SetupCredentialValue { + name: string; + value: string; +} +export type SetupCredentialDecision = 'keep' | 'replace' | 'skip'; +export interface SetupCredentialCollection { + workflowPat?: SetupCredentialValue; + apiKeys: SetupCredentialValue[]; +} +export interface SetupWorkflowComparison { + file: string; + destination: string; + status: 'missing' | 'unchanged' | 'changed' | 'unmanaged'; +} +export type DoctorCheckStatus = 'pass' | 'warn' | 'fail'; +export interface DoctorCheck { + area: string; + status: DoctorCheckStatus; + message: string; +} +export interface SetupVariable { + name: string; + value: string; +} +export interface SetupPlan { + configuration: SetupConfiguration; + workflowFiles: string[]; + issueTemplateFiles: string[]; + selectedFiles: string[]; + variables: SetupVariable[]; + requiredSecrets: string[]; + credentialRequirements: SetupCredentialRequirement[]; + warnings: string[]; +} diff --git a/build/cli/src/infrastructure/composition/github_identity_client_factory.d.ts b/build/cli/src/infrastructure/composition/github_identity_client_factory.d.ts index 4d1183cd..64c35141 100644 --- a/build/cli/src/infrastructure/composition/github_identity_client_factory.d.ts +++ b/build/cli/src/infrastructure/composition/github_identity_client_factory.d.ts @@ -1,4 +1,6 @@ import { OctokitAuthenticatedUserClientAdapter, OctokitActorAuthorizationClientAdapter, OctokitOrganizationMembersClientAdapter } from "../github/octokit_identity_adapters"; +import { OctokitRepositoryVariablesClientAdapter } from '../github/octokit_repository_variables_adapter'; export declare const createAuthenticatedUserClient: () => OctokitAuthenticatedUserClientAdapter; export declare const createActorAuthorizationClient: () => OctokitActorAuthorizationClientAdapter; export declare const createOrganizationMembersClient: () => OctokitOrganizationMembersClientAdapter; +export declare const createRepositoryVariablesClient: () => OctokitRepositoryVariablesClientAdapter; diff --git a/build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts b/build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts new file mode 100644 index 00000000..0c0bf531 --- /dev/null +++ b/build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts @@ -0,0 +1,3 @@ +import { SetupCredentialsUseCase } from '../../application/usecases/setup/setup_credentials_use_case'; +import type { SetupCredentialPromptPort } from '../../application/ports/setup_wizard_ports'; +export declare function createSetupCredentialsUseCase(prompt: SetupCredentialPromptPort): SetupCredentialsUseCase; diff --git a/build/cli/src/infrastructure/composition/setup_doctor_composition_root.d.ts b/build/cli/src/infrastructure/composition/setup_doctor_composition_root.d.ts new file mode 100644 index 00000000..ea0418e1 --- /dev/null +++ b/build/cli/src/infrastructure/composition/setup_doctor_composition_root.d.ts @@ -0,0 +1,3 @@ +import { SetupDoctorUseCase } from '../../application/usecases/setup/doctor_use_case'; +import type { DoctorOutputPort } from '../../application/ports/setup_wizard_ports'; +export declare function createSetupDoctorUseCase(output: DoctorOutputPort): SetupDoctorUseCase; diff --git a/build/cli/src/infrastructure/github/octokit_credential_health_adapter.d.ts b/build/cli/src/infrastructure/github/octokit_credential_health_adapter.d.ts new file mode 100644 index 00000000..07e458bb --- /dev/null +++ b/build/cli/src/infrastructure/github/octokit_credential_health_adapter.d.ts @@ -0,0 +1,5 @@ +import type { GithubClientPort } from './ports/github_client_provider_port'; +import type { GithubCredentialHealthClient } from './ports/github_credential_health_protocol'; +export declare class OctokitCredentialHealthClientAdapter implements GithubClientPort { + getClient(token: string): GithubCredentialHealthClient; +} diff --git a/build/cli/src/infrastructure/github/octokit_repository_variables_adapter.d.ts b/build/cli/src/infrastructure/github/octokit_repository_variables_adapter.d.ts new file mode 100644 index 00000000..7d77d208 --- /dev/null +++ b/build/cli/src/infrastructure/github/octokit_repository_variables_adapter.d.ts @@ -0,0 +1,5 @@ +import type { GithubClientPort } from './ports/github_client_provider_port'; +import type { GithubRepositoryVariablesClient } from './ports/github_repository_variables_protocol'; +export declare class OctokitRepositoryVariablesClientAdapter implements GithubClientPort { + getClient(token: string): GithubRepositoryVariablesClient; +} diff --git a/build/cli/src/infrastructure/github/ports/github_credential_health_protocol.d.ts b/build/cli/src/infrastructure/github/ports/github_credential_health_protocol.d.ts new file mode 100644 index 00000000..175b21ba --- /dev/null +++ b/build/cli/src/infrastructure/github/ports/github_credential_health_protocol.d.ts @@ -0,0 +1,52 @@ +export interface GithubCredentialHealthClient { + rest: { + actions: { + createWorkflowDispatch(parameters: Record): Promise; + listWorkflowRuns(parameters: Record): Promise<{ + data: { + workflow_runs: GithubWorkflowRun[]; + }; + }>; + getWorkflowRun(parameters: Record): Promise<{ + data: GithubWorkflowRun; + }>; + listJobsForWorkflowRun(parameters: Record): Promise<{ + data: { + jobs: GithubWorkflowJob[]; + }; + }>; + getWorkflow(parameters: Record): Promise; + }; + }; + repos: { + get(parameters: Record): Promise<{ + data: { + default_branch?: string; + }; + }>; + getContent(parameters: Record): Promise<{ + data: { + sha?: string; + }; + }>; + createOrUpdateFileContents(parameters: Record): Promise<{ + data?: { + content?: { + sha?: string; + }; + }; + }>; + deleteFile(parameters: Record): Promise; + }; +} +export interface GithubWorkflowRun { + id: number; + status?: string | null; + conclusion?: string | null; + created_at?: string; +} +export interface GithubWorkflowJob { + name: string; + status?: string | null; + conclusion?: string | null; +} diff --git a/build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts b/build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts new file mode 100644 index 00000000..37799fed --- /dev/null +++ b/build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts @@ -0,0 +1,36 @@ +export interface GithubRepositoryVariable { + name: string; + value?: string; +} +export interface GithubRepositoryVariablesClient { + rest: { + actions: { + listRepoVariables(parameters: Record): Promise<{ + data: { + variables: GithubRepositoryVariable[]; + }; + }>; + createRepoVariable(parameters: Record): Promise; + updateRepoVariable(parameters: Record): Promise; + }; + secrets?: { + listRepoSecrets(parameters: Record): Promise<{ + data: { + secrets: GithubRepositorySecret[]; + }; + }>; + getRepoPublicKey(parameters: Record): Promise<{ + data: { + key_id: string; + key: string; + }; + }>; + createOrUpdateRepoSecret(parameters: Record): Promise; + }; + }; +} +export interface GithubRepositorySecret { + name: string; + created_at?: string; + updated_at?: string; +} diff --git a/build/cli/src/infrastructure/setup_credential_validation_adapter.d.ts b/build/cli/src/infrastructure/setup_credential_validation_adapter.d.ts new file mode 100644 index 00000000..c14b72c9 --- /dev/null +++ b/build/cli/src/infrastructure/setup_credential_validation_adapter.d.ts @@ -0,0 +1,18 @@ +import type { SetupCredentialCheck, SetupCredentialRequirement } from '../domain/setup'; +import type { SetupCredentialValidationPort } from '../application/ports/setup_wizard_ports'; +export interface SetupCredentialValidationOptions { + fetcher?: typeof fetch; + timeoutMs?: number; +} +/** + * Performs bounded, metadata-only credential checks. Provider responses are + * intentionally never returned or logged because they can contain account data. + */ +export declare class SetupCredentialValidationAdapter implements SetupCredentialValidationPort { + private readonly fetcher; + private readonly timeoutMs; + constructor(options?: SetupCredentialValidationOptions); + validateSetupPat(owner: string, repository: string, token: string): Promise; + validateCredential(requirement: SetupCredentialRequirement, value: string): Promise; + private requestJson; +} diff --git a/build/cli/src/infrastructure/setup_remote_credential_health_adapter.d.ts b/build/cli/src/infrastructure/setup_remote_credential_health_adapter.d.ts new file mode 100644 index 00000000..71aa008a --- /dev/null +++ b/build/cli/src/infrastructure/setup_remote_credential_health_adapter.d.ts @@ -0,0 +1,25 @@ +import type { SetupCredentialCheck, SetupCredentialRequirement } from '../domain/setup'; +import type { SetupRemoteCredentialHealthPort } from '../application/ports/setup_wizard_ports'; +import type { GithubClientPort } from './github/ports/github_client_provider_port'; +import type { GithubCredentialHealthClient } from './github/ports/github_credential_health_protocol'; +export interface CredentialHealthAdapterOptions { + waitMs?: number; + pollMs?: number; + sleep?: (milliseconds: number) => Promise; + bootstrapWhenMissing?: boolean; + workflowContent?: string; +} +/** Dispatches the repository-owned health workflow; it cannot read or mutate Secret values. */ +export declare class SetupRemoteCredentialHealthAdapter implements SetupRemoteCredentialHealthPort { + private readonly githubClient; + private readonly waitMs; + private readonly pollMs; + private readonly sleep; + private readonly bootstrapWhenMissing; + private readonly workflowContent; + constructor(githubClient: GithubClientPort, options?: CredentialHealthAdapterOptions); + validateExisting(owner: string, repository: string, token: string, ref: string, requirements: readonly SetupCredentialRequirement[]): Promise; + private bootstrapWorkflow; + private removeTemporaryWorkflow; + private findRun; +} diff --git a/build/cli/src/infrastructure/setup_workspace_adapter.d.ts b/build/cli/src/infrastructure/setup_workspace_adapter.d.ts index 37acfe08..0b5e361b 100644 --- a/build/cli/src/infrastructure/setup_workspace_adapter.d.ts +++ b/build/cli/src/infrastructure/setup_workspace_adapter.d.ts @@ -1,5 +1,7 @@ -import type { SetupWorkspacePort, SetupWorkspaceResult } from '../application/ports/setup_workspace_ports'; +import { compareSetupWorkflows } from '../utils/setup_files'; +import type { SetupWorkspacePort, SetupWorkspaceResult, SetupWorkspaceSelection } from '../application/ports/setup_workspace_ports'; export declare class SetupWorkspaceAdapter implements SetupWorkspacePort { - prepare(): SetupWorkspaceResult; - hasValidToken(): boolean; + prepare(selection?: SetupWorkspaceSelection): SetupWorkspaceResult; + hasValidToken(tokenOverride?: string): boolean; + compareWorkflows(features?: Parameters[1]): ReturnType; } diff --git a/build/cli/src/utils/setup_file_copy.d.ts b/build/cli/src/utils/setup_file_copy.d.ts index df53c574..15b2e382 100644 --- a/build/cli/src/utils/setup_file_copy.d.ts +++ b/build/cli/src/utils/setup_file_copy.d.ts @@ -2,5 +2,9 @@ export type CopyStats = { copied: number; skipped: number; }; -export declare function copySetupFile(source: string, destination: string, displaySource: string, displayDestination: string): CopyStats; -export declare function copySetupDirectory(sourceDirectory: string, destinationDirectory: string, fileFilter: (fileName: string) => boolean, displayDirectory: string): CopyStats; +export interface CopyOptions { + overwrite?: boolean; + backupDirectory?: string; +} +export declare function copySetupFile(source: string, destination: string, displaySource: string, displayDestination: string, options?: CopyOptions): CopyStats; +export declare function copySetupDirectory(sourceDirectory: string, destinationDirectory: string, fileFilter: (fileName: string) => boolean, displayDirectory: string, options?: CopyOptions): CopyStats; diff --git a/build/cli/src/utils/setup_files.d.ts b/build/cli/src/utils/setup_files.d.ts index 2fdbeeef..a96dab6a 100644 --- a/build/cli/src/utils/setup_files.d.ts +++ b/build/cli/src/utils/setup_files.d.ts @@ -1,3 +1,4 @@ +import type { SetupFeatures, SetupWorkflowComparison } from '../domain/setup'; /** * Ensure .github, .github/workflows and .github/ISSUE_TEMPLATE exist; create them if missing. * @param cwd - Directory (repo root) @@ -12,26 +13,23 @@ export declare function ensureGitHubDirs(cwd: string): void; * @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root. * @returns { copied, skipped } */ -export declare function copySetupFiles(cwd: string, setupDirOverride?: string): { +export declare function copySetupFiles(cwd: string, setupDirOverride?: string, features?: SetupFeatures, options?: { + updateExistingWorkflows?: boolean; + approvedWorkflowFiles?: readonly string[]; +}): { copied: number; skipped: number; }; -/** - * Logs the current state of PERSONAL_ACCESS_TOKEN (environment or .env). Does not create .env. - */ -export declare function ensureEnvWithToken(cwd: string): void; +export declare function compareSetupWorkflows(cwd: string, features?: SetupFeatures, setupDirOverride?: string): SetupWorkflowComparison[]; /** * Resolves the PERSONAL_ACCESS_TOKEN for setup from a single priority order: * 1. override (e.g. CLI --token) if provided and valid, - * 2. process.env.PERSONAL_ACCESS_TOKEN, - * 3. .env file in cwd. + * 2. process.env.PERSONAL_ACCESS_TOKEN. * Returns undefined if no valid token is found. */ -export declare function getSetupToken(cwd: string, override?: string): string | undefined; +export declare function getSetupToken(_cwd: string, override?: string): string | undefined; /** * Returns true if a valid setup token is available (same resolution order as getSetupToken). * Pass an optional override (e.g. CLI --token) so validation considers all sources consistently. */ export declare function hasValidSetupToken(cwd: string, override?: string): boolean; -/** Returns true if a .env file exists in the given directory. */ -export declare function setupEnvFileExists(cwd: string): boolean; diff --git a/build/github_action/index.js b/build/github_action/index.js index 48d3fac0..140147f5 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -20065,6 +20065,2404 @@ if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { exports.debug = debug; // for test +/***/ }), + +/***/ 24258: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +(function(nacl) { +'use strict'; + +// Ported in 2014 by Dmitry Chestnykh and Devi Mandiri. +// Public domain. +// +// Implementation derived from TweetNaCl version 20140427. +// See for details: http://tweetnacl.cr.yp.to/ + +var gf = function(init) { + var i, r = new Float64Array(16); + if (init) for (i = 0; i < init.length; i++) r[i] = init[i]; + return r; +}; + +// Pluggable, initialized in high-level API below. +var randombytes = function(/* x, n */) { throw new Error('no PRNG'); }; + +var _0 = new Uint8Array(16); +var _9 = new Uint8Array(32); _9[0] = 9; + +var gf0 = gf(), + gf1 = gf([1]), + _121665 = gf([0xdb41, 1]), + D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]), + D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]), + X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]), + Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]), + I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]); + +function ts64(x, i, h, l) { + x[i] = (h >> 24) & 0xff; + x[i+1] = (h >> 16) & 0xff; + x[i+2] = (h >> 8) & 0xff; + x[i+3] = h & 0xff; + x[i+4] = (l >> 24) & 0xff; + x[i+5] = (l >> 16) & 0xff; + x[i+6] = (l >> 8) & 0xff; + x[i+7] = l & 0xff; +} + +function vn(x, xi, y, yi, n) { + var i,d = 0; + for (i = 0; i < n; i++) d |= x[xi+i]^y[yi+i]; + return (1 & ((d - 1) >>> 8)) - 1; +} + +function crypto_verify_16(x, xi, y, yi) { + return vn(x,xi,y,yi,16); +} + +function crypto_verify_32(x, xi, y, yi) { + return vn(x,xi,y,yi,32); +} + +function core_salsa20(o, p, k, c) { + var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; + + var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u; + + for (var i = 0; i < 20; i += 2) { + u = x0 + x12 | 0; + x4 ^= u<<7 | u>>>(32-7); + u = x4 + x0 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x4 | 0; + x12 ^= u<<13 | u>>>(32-13); + u = x12 + x8 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x1 | 0; + x9 ^= u<<7 | u>>>(32-7); + u = x9 + x5 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x9 | 0; + x1 ^= u<<13 | u>>>(32-13); + u = x1 + x13 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x6 | 0; + x14 ^= u<<7 | u>>>(32-7); + u = x14 + x10 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x14 | 0; + x6 ^= u<<13 | u>>>(32-13); + u = x6 + x2 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x11 | 0; + x3 ^= u<<7 | u>>>(32-7); + u = x3 + x15 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x3 | 0; + x11 ^= u<<13 | u>>>(32-13); + u = x11 + x7 | 0; + x15 ^= u<<18 | u>>>(32-18); + + u = x0 + x3 | 0; + x1 ^= u<<7 | u>>>(32-7); + u = x1 + x0 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x1 | 0; + x3 ^= u<<13 | u>>>(32-13); + u = x3 + x2 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x4 | 0; + x6 ^= u<<7 | u>>>(32-7); + u = x6 + x5 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x6 | 0; + x4 ^= u<<13 | u>>>(32-13); + u = x4 + x7 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x9 | 0; + x11 ^= u<<7 | u>>>(32-7); + u = x11 + x10 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x11 | 0; + x9 ^= u<<13 | u>>>(32-13); + u = x9 + x8 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x14 | 0; + x12 ^= u<<7 | u>>>(32-7); + u = x12 + x15 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x12 | 0; + x14 ^= u<<13 | u>>>(32-13); + u = x14 + x13 | 0; + x15 ^= u<<18 | u>>>(32-18); + } + x0 = x0 + j0 | 0; + x1 = x1 + j1 | 0; + x2 = x2 + j2 | 0; + x3 = x3 + j3 | 0; + x4 = x4 + j4 | 0; + x5 = x5 + j5 | 0; + x6 = x6 + j6 | 0; + x7 = x7 + j7 | 0; + x8 = x8 + j8 | 0; + x9 = x9 + j9 | 0; + x10 = x10 + j10 | 0; + x11 = x11 + j11 | 0; + x12 = x12 + j12 | 0; + x13 = x13 + j13 | 0; + x14 = x14 + j14 | 0; + x15 = x15 + j15 | 0; + + o[ 0] = x0 >>> 0 & 0xff; + o[ 1] = x0 >>> 8 & 0xff; + o[ 2] = x0 >>> 16 & 0xff; + o[ 3] = x0 >>> 24 & 0xff; + + o[ 4] = x1 >>> 0 & 0xff; + o[ 5] = x1 >>> 8 & 0xff; + o[ 6] = x1 >>> 16 & 0xff; + o[ 7] = x1 >>> 24 & 0xff; + + o[ 8] = x2 >>> 0 & 0xff; + o[ 9] = x2 >>> 8 & 0xff; + o[10] = x2 >>> 16 & 0xff; + o[11] = x2 >>> 24 & 0xff; + + o[12] = x3 >>> 0 & 0xff; + o[13] = x3 >>> 8 & 0xff; + o[14] = x3 >>> 16 & 0xff; + o[15] = x3 >>> 24 & 0xff; + + o[16] = x4 >>> 0 & 0xff; + o[17] = x4 >>> 8 & 0xff; + o[18] = x4 >>> 16 & 0xff; + o[19] = x4 >>> 24 & 0xff; + + o[20] = x5 >>> 0 & 0xff; + o[21] = x5 >>> 8 & 0xff; + o[22] = x5 >>> 16 & 0xff; + o[23] = x5 >>> 24 & 0xff; + + o[24] = x6 >>> 0 & 0xff; + o[25] = x6 >>> 8 & 0xff; + o[26] = x6 >>> 16 & 0xff; + o[27] = x6 >>> 24 & 0xff; + + o[28] = x7 >>> 0 & 0xff; + o[29] = x7 >>> 8 & 0xff; + o[30] = x7 >>> 16 & 0xff; + o[31] = x7 >>> 24 & 0xff; + + o[32] = x8 >>> 0 & 0xff; + o[33] = x8 >>> 8 & 0xff; + o[34] = x8 >>> 16 & 0xff; + o[35] = x8 >>> 24 & 0xff; + + o[36] = x9 >>> 0 & 0xff; + o[37] = x9 >>> 8 & 0xff; + o[38] = x9 >>> 16 & 0xff; + o[39] = x9 >>> 24 & 0xff; + + o[40] = x10 >>> 0 & 0xff; + o[41] = x10 >>> 8 & 0xff; + o[42] = x10 >>> 16 & 0xff; + o[43] = x10 >>> 24 & 0xff; + + o[44] = x11 >>> 0 & 0xff; + o[45] = x11 >>> 8 & 0xff; + o[46] = x11 >>> 16 & 0xff; + o[47] = x11 >>> 24 & 0xff; + + o[48] = x12 >>> 0 & 0xff; + o[49] = x12 >>> 8 & 0xff; + o[50] = x12 >>> 16 & 0xff; + o[51] = x12 >>> 24 & 0xff; + + o[52] = x13 >>> 0 & 0xff; + o[53] = x13 >>> 8 & 0xff; + o[54] = x13 >>> 16 & 0xff; + o[55] = x13 >>> 24 & 0xff; + + o[56] = x14 >>> 0 & 0xff; + o[57] = x14 >>> 8 & 0xff; + o[58] = x14 >>> 16 & 0xff; + o[59] = x14 >>> 24 & 0xff; + + o[60] = x15 >>> 0 & 0xff; + o[61] = x15 >>> 8 & 0xff; + o[62] = x15 >>> 16 & 0xff; + o[63] = x15 >>> 24 & 0xff; +} + +function core_hsalsa20(o,p,k,c) { + var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; + + var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u; + + for (var i = 0; i < 20; i += 2) { + u = x0 + x12 | 0; + x4 ^= u<<7 | u>>>(32-7); + u = x4 + x0 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x4 | 0; + x12 ^= u<<13 | u>>>(32-13); + u = x12 + x8 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x1 | 0; + x9 ^= u<<7 | u>>>(32-7); + u = x9 + x5 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x9 | 0; + x1 ^= u<<13 | u>>>(32-13); + u = x1 + x13 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x6 | 0; + x14 ^= u<<7 | u>>>(32-7); + u = x14 + x10 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x14 | 0; + x6 ^= u<<13 | u>>>(32-13); + u = x6 + x2 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x11 | 0; + x3 ^= u<<7 | u>>>(32-7); + u = x3 + x15 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x3 | 0; + x11 ^= u<<13 | u>>>(32-13); + u = x11 + x7 | 0; + x15 ^= u<<18 | u>>>(32-18); + + u = x0 + x3 | 0; + x1 ^= u<<7 | u>>>(32-7); + u = x1 + x0 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x1 | 0; + x3 ^= u<<13 | u>>>(32-13); + u = x3 + x2 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x4 | 0; + x6 ^= u<<7 | u>>>(32-7); + u = x6 + x5 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x6 | 0; + x4 ^= u<<13 | u>>>(32-13); + u = x4 + x7 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x9 | 0; + x11 ^= u<<7 | u>>>(32-7); + u = x11 + x10 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x11 | 0; + x9 ^= u<<13 | u>>>(32-13); + u = x9 + x8 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x14 | 0; + x12 ^= u<<7 | u>>>(32-7); + u = x12 + x15 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x12 | 0; + x14 ^= u<<13 | u>>>(32-13); + u = x14 + x13 | 0; + x15 ^= u<<18 | u>>>(32-18); + } + + o[ 0] = x0 >>> 0 & 0xff; + o[ 1] = x0 >>> 8 & 0xff; + o[ 2] = x0 >>> 16 & 0xff; + o[ 3] = x0 >>> 24 & 0xff; + + o[ 4] = x5 >>> 0 & 0xff; + o[ 5] = x5 >>> 8 & 0xff; + o[ 6] = x5 >>> 16 & 0xff; + o[ 7] = x5 >>> 24 & 0xff; + + o[ 8] = x10 >>> 0 & 0xff; + o[ 9] = x10 >>> 8 & 0xff; + o[10] = x10 >>> 16 & 0xff; + o[11] = x10 >>> 24 & 0xff; + + o[12] = x15 >>> 0 & 0xff; + o[13] = x15 >>> 8 & 0xff; + o[14] = x15 >>> 16 & 0xff; + o[15] = x15 >>> 24 & 0xff; + + o[16] = x6 >>> 0 & 0xff; + o[17] = x6 >>> 8 & 0xff; + o[18] = x6 >>> 16 & 0xff; + o[19] = x6 >>> 24 & 0xff; + + o[20] = x7 >>> 0 & 0xff; + o[21] = x7 >>> 8 & 0xff; + o[22] = x7 >>> 16 & 0xff; + o[23] = x7 >>> 24 & 0xff; + + o[24] = x8 >>> 0 & 0xff; + o[25] = x8 >>> 8 & 0xff; + o[26] = x8 >>> 16 & 0xff; + o[27] = x8 >>> 24 & 0xff; + + o[28] = x9 >>> 0 & 0xff; + o[29] = x9 >>> 8 & 0xff; + o[30] = x9 >>> 16 & 0xff; + o[31] = x9 >>> 24 & 0xff; +} + +function crypto_core_salsa20(out,inp,k,c) { + core_salsa20(out,inp,k,c); +} + +function crypto_core_hsalsa20(out,inp,k,c) { + core_hsalsa20(out,inp,k,c); +} + +var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]); + // "expand 32-byte k" + +function crypto_stream_salsa20_xor(c,cpos,m,mpos,b,n,k) { + var z = new Uint8Array(16), x = new Uint8Array(64); + var u, i; + for (i = 0; i < 16; i++) z[i] = 0; + for (i = 0; i < 8; i++) z[i] = n[i]; + while (b >= 64) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < 64; i++) c[cpos+i] = m[mpos+i] ^ x[i]; + u = 1; + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0; + z[i] = u & 0xff; + u >>>= 8; + } + b -= 64; + cpos += 64; + mpos += 64; + } + if (b > 0) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < b; i++) c[cpos+i] = m[mpos+i] ^ x[i]; + } + return 0; +} + +function crypto_stream_salsa20(c,cpos,b,n,k) { + var z = new Uint8Array(16), x = new Uint8Array(64); + var u, i; + for (i = 0; i < 16; i++) z[i] = 0; + for (i = 0; i < 8; i++) z[i] = n[i]; + while (b >= 64) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < 64; i++) c[cpos+i] = x[i]; + u = 1; + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0; + z[i] = u & 0xff; + u >>>= 8; + } + b -= 64; + cpos += 64; + } + if (b > 0) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < b; i++) c[cpos+i] = x[i]; + } + return 0; +} + +function crypto_stream(c,cpos,d,n,k) { + var s = new Uint8Array(32); + crypto_core_hsalsa20(s,n,k,sigma); + var sn = new Uint8Array(8); + for (var i = 0; i < 8; i++) sn[i] = n[i+16]; + return crypto_stream_salsa20(c,cpos,d,sn,s); +} + +function crypto_stream_xor(c,cpos,m,mpos,d,n,k) { + var s = new Uint8Array(32); + crypto_core_hsalsa20(s,n,k,sigma); + var sn = new Uint8Array(8); + for (var i = 0; i < 8; i++) sn[i] = n[i+16]; + return crypto_stream_salsa20_xor(c,cpos,m,mpos,d,sn,s); +} + +/* +* Port of Andrew Moon's Poly1305-donna-16. Public domain. +* https://github.com/floodyberry/poly1305-donna +*/ + +var poly1305 = function(key) { + this.buffer = new Uint8Array(16); + this.r = new Uint16Array(10); + this.h = new Uint16Array(10); + this.pad = new Uint16Array(8); + this.leftover = 0; + this.fin = 0; + + var t0, t1, t2, t3, t4, t5, t6, t7; + + t0 = key[ 0] & 0xff | (key[ 1] & 0xff) << 8; this.r[0] = ( t0 ) & 0x1fff; + t1 = key[ 2] & 0xff | (key[ 3] & 0xff) << 8; this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff; + t2 = key[ 4] & 0xff | (key[ 5] & 0xff) << 8; this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03; + t3 = key[ 6] & 0xff | (key[ 7] & 0xff) << 8; this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff; + t4 = key[ 8] & 0xff | (key[ 9] & 0xff) << 8; this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff; + this.r[5] = ((t4 >>> 1)) & 0x1ffe; + t5 = key[10] & 0xff | (key[11] & 0xff) << 8; this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff; + t6 = key[12] & 0xff | (key[13] & 0xff) << 8; this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81; + t7 = key[14] & 0xff | (key[15] & 0xff) << 8; this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff; + this.r[9] = ((t7 >>> 5)) & 0x007f; + + this.pad[0] = key[16] & 0xff | (key[17] & 0xff) << 8; + this.pad[1] = key[18] & 0xff | (key[19] & 0xff) << 8; + this.pad[2] = key[20] & 0xff | (key[21] & 0xff) << 8; + this.pad[3] = key[22] & 0xff | (key[23] & 0xff) << 8; + this.pad[4] = key[24] & 0xff | (key[25] & 0xff) << 8; + this.pad[5] = key[26] & 0xff | (key[27] & 0xff) << 8; + this.pad[6] = key[28] & 0xff | (key[29] & 0xff) << 8; + this.pad[7] = key[30] & 0xff | (key[31] & 0xff) << 8; +}; + +poly1305.prototype.blocks = function(m, mpos, bytes) { + var hibit = this.fin ? 0 : (1 << 11); + var t0, t1, t2, t3, t4, t5, t6, t7, c; + var d0, d1, d2, d3, d4, d5, d6, d7, d8, d9; + + var h0 = this.h[0], + h1 = this.h[1], + h2 = this.h[2], + h3 = this.h[3], + h4 = this.h[4], + h5 = this.h[5], + h6 = this.h[6], + h7 = this.h[7], + h8 = this.h[8], + h9 = this.h[9]; + + var r0 = this.r[0], + r1 = this.r[1], + r2 = this.r[2], + r3 = this.r[3], + r4 = this.r[4], + r5 = this.r[5], + r6 = this.r[6], + r7 = this.r[7], + r8 = this.r[8], + r9 = this.r[9]; + + while (bytes >= 16) { + t0 = m[mpos+ 0] & 0xff | (m[mpos+ 1] & 0xff) << 8; h0 += ( t0 ) & 0x1fff; + t1 = m[mpos+ 2] & 0xff | (m[mpos+ 3] & 0xff) << 8; h1 += ((t0 >>> 13) | (t1 << 3)) & 0x1fff; + t2 = m[mpos+ 4] & 0xff | (m[mpos+ 5] & 0xff) << 8; h2 += ((t1 >>> 10) | (t2 << 6)) & 0x1fff; + t3 = m[mpos+ 6] & 0xff | (m[mpos+ 7] & 0xff) << 8; h3 += ((t2 >>> 7) | (t3 << 9)) & 0x1fff; + t4 = m[mpos+ 8] & 0xff | (m[mpos+ 9] & 0xff) << 8; h4 += ((t3 >>> 4) | (t4 << 12)) & 0x1fff; + h5 += ((t4 >>> 1)) & 0x1fff; + t5 = m[mpos+10] & 0xff | (m[mpos+11] & 0xff) << 8; h6 += ((t4 >>> 14) | (t5 << 2)) & 0x1fff; + t6 = m[mpos+12] & 0xff | (m[mpos+13] & 0xff) << 8; h7 += ((t5 >>> 11) | (t6 << 5)) & 0x1fff; + t7 = m[mpos+14] & 0xff | (m[mpos+15] & 0xff) << 8; h8 += ((t6 >>> 8) | (t7 << 8)) & 0x1fff; + h9 += ((t7 >>> 5)) | hibit; + + c = 0; + + d0 = c; + d0 += h0 * r0; + d0 += h1 * (5 * r9); + d0 += h2 * (5 * r8); + d0 += h3 * (5 * r7); + d0 += h4 * (5 * r6); + c = (d0 >>> 13); d0 &= 0x1fff; + d0 += h5 * (5 * r5); + d0 += h6 * (5 * r4); + d0 += h7 * (5 * r3); + d0 += h8 * (5 * r2); + d0 += h9 * (5 * r1); + c += (d0 >>> 13); d0 &= 0x1fff; + + d1 = c; + d1 += h0 * r1; + d1 += h1 * r0; + d1 += h2 * (5 * r9); + d1 += h3 * (5 * r8); + d1 += h4 * (5 * r7); + c = (d1 >>> 13); d1 &= 0x1fff; + d1 += h5 * (5 * r6); + d1 += h6 * (5 * r5); + d1 += h7 * (5 * r4); + d1 += h8 * (5 * r3); + d1 += h9 * (5 * r2); + c += (d1 >>> 13); d1 &= 0x1fff; + + d2 = c; + d2 += h0 * r2; + d2 += h1 * r1; + d2 += h2 * r0; + d2 += h3 * (5 * r9); + d2 += h4 * (5 * r8); + c = (d2 >>> 13); d2 &= 0x1fff; + d2 += h5 * (5 * r7); + d2 += h6 * (5 * r6); + d2 += h7 * (5 * r5); + d2 += h8 * (5 * r4); + d2 += h9 * (5 * r3); + c += (d2 >>> 13); d2 &= 0x1fff; + + d3 = c; + d3 += h0 * r3; + d3 += h1 * r2; + d3 += h2 * r1; + d3 += h3 * r0; + d3 += h4 * (5 * r9); + c = (d3 >>> 13); d3 &= 0x1fff; + d3 += h5 * (5 * r8); + d3 += h6 * (5 * r7); + d3 += h7 * (5 * r6); + d3 += h8 * (5 * r5); + d3 += h9 * (5 * r4); + c += (d3 >>> 13); d3 &= 0x1fff; + + d4 = c; + d4 += h0 * r4; + d4 += h1 * r3; + d4 += h2 * r2; + d4 += h3 * r1; + d4 += h4 * r0; + c = (d4 >>> 13); d4 &= 0x1fff; + d4 += h5 * (5 * r9); + d4 += h6 * (5 * r8); + d4 += h7 * (5 * r7); + d4 += h8 * (5 * r6); + d4 += h9 * (5 * r5); + c += (d4 >>> 13); d4 &= 0x1fff; + + d5 = c; + d5 += h0 * r5; + d5 += h1 * r4; + d5 += h2 * r3; + d5 += h3 * r2; + d5 += h4 * r1; + c = (d5 >>> 13); d5 &= 0x1fff; + d5 += h5 * r0; + d5 += h6 * (5 * r9); + d5 += h7 * (5 * r8); + d5 += h8 * (5 * r7); + d5 += h9 * (5 * r6); + c += (d5 >>> 13); d5 &= 0x1fff; + + d6 = c; + d6 += h0 * r6; + d6 += h1 * r5; + d6 += h2 * r4; + d6 += h3 * r3; + d6 += h4 * r2; + c = (d6 >>> 13); d6 &= 0x1fff; + d6 += h5 * r1; + d6 += h6 * r0; + d6 += h7 * (5 * r9); + d6 += h8 * (5 * r8); + d6 += h9 * (5 * r7); + c += (d6 >>> 13); d6 &= 0x1fff; + + d7 = c; + d7 += h0 * r7; + d7 += h1 * r6; + d7 += h2 * r5; + d7 += h3 * r4; + d7 += h4 * r3; + c = (d7 >>> 13); d7 &= 0x1fff; + d7 += h5 * r2; + d7 += h6 * r1; + d7 += h7 * r0; + d7 += h8 * (5 * r9); + d7 += h9 * (5 * r8); + c += (d7 >>> 13); d7 &= 0x1fff; + + d8 = c; + d8 += h0 * r8; + d8 += h1 * r7; + d8 += h2 * r6; + d8 += h3 * r5; + d8 += h4 * r4; + c = (d8 >>> 13); d8 &= 0x1fff; + d8 += h5 * r3; + d8 += h6 * r2; + d8 += h7 * r1; + d8 += h8 * r0; + d8 += h9 * (5 * r9); + c += (d8 >>> 13); d8 &= 0x1fff; + + d9 = c; + d9 += h0 * r9; + d9 += h1 * r8; + d9 += h2 * r7; + d9 += h3 * r6; + d9 += h4 * r5; + c = (d9 >>> 13); d9 &= 0x1fff; + d9 += h5 * r4; + d9 += h6 * r3; + d9 += h7 * r2; + d9 += h8 * r1; + d9 += h9 * r0; + c += (d9 >>> 13); d9 &= 0x1fff; + + c = (((c << 2) + c)) | 0; + c = (c + d0) | 0; + d0 = c & 0x1fff; + c = (c >>> 13); + d1 += c; + + h0 = d0; + h1 = d1; + h2 = d2; + h3 = d3; + h4 = d4; + h5 = d5; + h6 = d6; + h7 = d7; + h8 = d8; + h9 = d9; + + mpos += 16; + bytes -= 16; + } + this.h[0] = h0; + this.h[1] = h1; + this.h[2] = h2; + this.h[3] = h3; + this.h[4] = h4; + this.h[5] = h5; + this.h[6] = h6; + this.h[7] = h7; + this.h[8] = h8; + this.h[9] = h9; +}; + +poly1305.prototype.finish = function(mac, macpos) { + var g = new Uint16Array(10); + var c, mask, f, i; + + if (this.leftover) { + i = this.leftover; + this.buffer[i++] = 1; + for (; i < 16; i++) this.buffer[i] = 0; + this.fin = 1; + this.blocks(this.buffer, 0, 16); + } + + c = this.h[1] >>> 13; + this.h[1] &= 0x1fff; + for (i = 2; i < 10; i++) { + this.h[i] += c; + c = this.h[i] >>> 13; + this.h[i] &= 0x1fff; + } + this.h[0] += (c * 5); + c = this.h[0] >>> 13; + this.h[0] &= 0x1fff; + this.h[1] += c; + c = this.h[1] >>> 13; + this.h[1] &= 0x1fff; + this.h[2] += c; + + g[0] = this.h[0] + 5; + c = g[0] >>> 13; + g[0] &= 0x1fff; + for (i = 1; i < 10; i++) { + g[i] = this.h[i] + c; + c = g[i] >>> 13; + g[i] &= 0x1fff; + } + g[9] -= (1 << 13); + + mask = (c ^ 1) - 1; + for (i = 0; i < 10; i++) g[i] &= mask; + mask = ~mask; + for (i = 0; i < 10; i++) this.h[i] = (this.h[i] & mask) | g[i]; + + this.h[0] = ((this.h[0] ) | (this.h[1] << 13) ) & 0xffff; + this.h[1] = ((this.h[1] >>> 3) | (this.h[2] << 10) ) & 0xffff; + this.h[2] = ((this.h[2] >>> 6) | (this.h[3] << 7) ) & 0xffff; + this.h[3] = ((this.h[3] >>> 9) | (this.h[4] << 4) ) & 0xffff; + this.h[4] = ((this.h[4] >>> 12) | (this.h[5] << 1) | (this.h[6] << 14)) & 0xffff; + this.h[5] = ((this.h[6] >>> 2) | (this.h[7] << 11) ) & 0xffff; + this.h[6] = ((this.h[7] >>> 5) | (this.h[8] << 8) ) & 0xffff; + this.h[7] = ((this.h[8] >>> 8) | (this.h[9] << 5) ) & 0xffff; + + f = this.h[0] + this.pad[0]; + this.h[0] = f & 0xffff; + for (i = 1; i < 8; i++) { + f = (((this.h[i] + this.pad[i]) | 0) + (f >>> 16)) | 0; + this.h[i] = f & 0xffff; + } + + mac[macpos+ 0] = (this.h[0] >>> 0) & 0xff; + mac[macpos+ 1] = (this.h[0] >>> 8) & 0xff; + mac[macpos+ 2] = (this.h[1] >>> 0) & 0xff; + mac[macpos+ 3] = (this.h[1] >>> 8) & 0xff; + mac[macpos+ 4] = (this.h[2] >>> 0) & 0xff; + mac[macpos+ 5] = (this.h[2] >>> 8) & 0xff; + mac[macpos+ 6] = (this.h[3] >>> 0) & 0xff; + mac[macpos+ 7] = (this.h[3] >>> 8) & 0xff; + mac[macpos+ 8] = (this.h[4] >>> 0) & 0xff; + mac[macpos+ 9] = (this.h[4] >>> 8) & 0xff; + mac[macpos+10] = (this.h[5] >>> 0) & 0xff; + mac[macpos+11] = (this.h[5] >>> 8) & 0xff; + mac[macpos+12] = (this.h[6] >>> 0) & 0xff; + mac[macpos+13] = (this.h[6] >>> 8) & 0xff; + mac[macpos+14] = (this.h[7] >>> 0) & 0xff; + mac[macpos+15] = (this.h[7] >>> 8) & 0xff; +}; + +poly1305.prototype.update = function(m, mpos, bytes) { + var i, want; + + if (this.leftover) { + want = (16 - this.leftover); + if (want > bytes) + want = bytes; + for (i = 0; i < want; i++) + this.buffer[this.leftover + i] = m[mpos+i]; + bytes -= want; + mpos += want; + this.leftover += want; + if (this.leftover < 16) + return; + this.blocks(this.buffer, 0, 16); + this.leftover = 0; + } + + if (bytes >= 16) { + want = bytes - (bytes % 16); + this.blocks(m, mpos, want); + mpos += want; + bytes -= want; + } + + if (bytes) { + for (i = 0; i < bytes; i++) + this.buffer[this.leftover + i] = m[mpos+i]; + this.leftover += bytes; + } +}; + +function crypto_onetimeauth(out, outpos, m, mpos, n, k) { + var s = new poly1305(k); + s.update(m, mpos, n); + s.finish(out, outpos); + return 0; +} + +function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) { + var x = new Uint8Array(16); + crypto_onetimeauth(x,0,m,mpos,n,k); + return crypto_verify_16(h,hpos,x,0); +} + +function crypto_secretbox(c,m,d,n,k) { + var i; + if (d < 32) return -1; + crypto_stream_xor(c,0,m,0,d,n,k); + crypto_onetimeauth(c, 16, c, 32, d - 32, c); + for (i = 0; i < 16; i++) c[i] = 0; + return 0; +} + +function crypto_secretbox_open(m,c,d,n,k) { + var i; + var x = new Uint8Array(32); + if (d < 32) return -1; + crypto_stream(x,0,32,n,k); + if (crypto_onetimeauth_verify(c, 16,c, 32,d - 32,x) !== 0) return -1; + crypto_stream_xor(m,0,c,0,d,n,k); + for (i = 0; i < 32; i++) m[i] = 0; + return 0; +} + +function set25519(r, a) { + var i; + for (i = 0; i < 16; i++) r[i] = a[i]|0; +} + +function car25519(o) { + var i, v, c = 1; + for (i = 0; i < 16; i++) { + v = o[i] + c + 65535; + c = Math.floor(v / 65536); + o[i] = v - c * 65536; + } + o[0] += c-1 + 37 * (c-1); +} + +function sel25519(p, q, b) { + var t, c = ~(b-1); + for (var i = 0; i < 16; i++) { + t = c & (p[i] ^ q[i]); + p[i] ^= t; + q[i] ^= t; + } +} + +function pack25519(o, n) { + var i, j, b; + var m = gf(), t = gf(); + for (i = 0; i < 16; i++) t[i] = n[i]; + car25519(t); + car25519(t); + car25519(t); + for (j = 0; j < 2; j++) { + m[0] = t[0] - 0xffed; + for (i = 1; i < 15; i++) { + m[i] = t[i] - 0xffff - ((m[i-1]>>16) & 1); + m[i-1] &= 0xffff; + } + m[15] = t[15] - 0x7fff - ((m[14]>>16) & 1); + b = (m[15]>>16) & 1; + m[14] &= 0xffff; + sel25519(t, m, 1-b); + } + for (i = 0; i < 16; i++) { + o[2*i] = t[i] & 0xff; + o[2*i+1] = t[i]>>8; + } +} + +function neq25519(a, b) { + var c = new Uint8Array(32), d = new Uint8Array(32); + pack25519(c, a); + pack25519(d, b); + return crypto_verify_32(c, 0, d, 0); +} + +function par25519(a) { + var d = new Uint8Array(32); + pack25519(d, a); + return d[0] & 1; +} + +function unpack25519(o, n) { + var i; + for (i = 0; i < 16; i++) o[i] = n[2*i] + (n[2*i+1] << 8); + o[15] &= 0x7fff; +} + +function A(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] + b[i]; +} + +function Z(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] - b[i]; +} + +function M(o, a, b) { + var v, c, + t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, + t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0, + t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0, + t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0, + b0 = b[0], + b1 = b[1], + b2 = b[2], + b3 = b[3], + b4 = b[4], + b5 = b[5], + b6 = b[6], + b7 = b[7], + b8 = b[8], + b9 = b[9], + b10 = b[10], + b11 = b[11], + b12 = b[12], + b13 = b[13], + b14 = b[14], + b15 = b[15]; + + v = a[0]; + t0 += v * b0; + t1 += v * b1; + t2 += v * b2; + t3 += v * b3; + t4 += v * b4; + t5 += v * b5; + t6 += v * b6; + t7 += v * b7; + t8 += v * b8; + t9 += v * b9; + t10 += v * b10; + t11 += v * b11; + t12 += v * b12; + t13 += v * b13; + t14 += v * b14; + t15 += v * b15; + v = a[1]; + t1 += v * b0; + t2 += v * b1; + t3 += v * b2; + t4 += v * b3; + t5 += v * b4; + t6 += v * b5; + t7 += v * b6; + t8 += v * b7; + t9 += v * b8; + t10 += v * b9; + t11 += v * b10; + t12 += v * b11; + t13 += v * b12; + t14 += v * b13; + t15 += v * b14; + t16 += v * b15; + v = a[2]; + t2 += v * b0; + t3 += v * b1; + t4 += v * b2; + t5 += v * b3; + t6 += v * b4; + t7 += v * b5; + t8 += v * b6; + t9 += v * b7; + t10 += v * b8; + t11 += v * b9; + t12 += v * b10; + t13 += v * b11; + t14 += v * b12; + t15 += v * b13; + t16 += v * b14; + t17 += v * b15; + v = a[3]; + t3 += v * b0; + t4 += v * b1; + t5 += v * b2; + t6 += v * b3; + t7 += v * b4; + t8 += v * b5; + t9 += v * b6; + t10 += v * b7; + t11 += v * b8; + t12 += v * b9; + t13 += v * b10; + t14 += v * b11; + t15 += v * b12; + t16 += v * b13; + t17 += v * b14; + t18 += v * b15; + v = a[4]; + t4 += v * b0; + t5 += v * b1; + t6 += v * b2; + t7 += v * b3; + t8 += v * b4; + t9 += v * b5; + t10 += v * b6; + t11 += v * b7; + t12 += v * b8; + t13 += v * b9; + t14 += v * b10; + t15 += v * b11; + t16 += v * b12; + t17 += v * b13; + t18 += v * b14; + t19 += v * b15; + v = a[5]; + t5 += v * b0; + t6 += v * b1; + t7 += v * b2; + t8 += v * b3; + t9 += v * b4; + t10 += v * b5; + t11 += v * b6; + t12 += v * b7; + t13 += v * b8; + t14 += v * b9; + t15 += v * b10; + t16 += v * b11; + t17 += v * b12; + t18 += v * b13; + t19 += v * b14; + t20 += v * b15; + v = a[6]; + t6 += v * b0; + t7 += v * b1; + t8 += v * b2; + t9 += v * b3; + t10 += v * b4; + t11 += v * b5; + t12 += v * b6; + t13 += v * b7; + t14 += v * b8; + t15 += v * b9; + t16 += v * b10; + t17 += v * b11; + t18 += v * b12; + t19 += v * b13; + t20 += v * b14; + t21 += v * b15; + v = a[7]; + t7 += v * b0; + t8 += v * b1; + t9 += v * b2; + t10 += v * b3; + t11 += v * b4; + t12 += v * b5; + t13 += v * b6; + t14 += v * b7; + t15 += v * b8; + t16 += v * b9; + t17 += v * b10; + t18 += v * b11; + t19 += v * b12; + t20 += v * b13; + t21 += v * b14; + t22 += v * b15; + v = a[8]; + t8 += v * b0; + t9 += v * b1; + t10 += v * b2; + t11 += v * b3; + t12 += v * b4; + t13 += v * b5; + t14 += v * b6; + t15 += v * b7; + t16 += v * b8; + t17 += v * b9; + t18 += v * b10; + t19 += v * b11; + t20 += v * b12; + t21 += v * b13; + t22 += v * b14; + t23 += v * b15; + v = a[9]; + t9 += v * b0; + t10 += v * b1; + t11 += v * b2; + t12 += v * b3; + t13 += v * b4; + t14 += v * b5; + t15 += v * b6; + t16 += v * b7; + t17 += v * b8; + t18 += v * b9; + t19 += v * b10; + t20 += v * b11; + t21 += v * b12; + t22 += v * b13; + t23 += v * b14; + t24 += v * b15; + v = a[10]; + t10 += v * b0; + t11 += v * b1; + t12 += v * b2; + t13 += v * b3; + t14 += v * b4; + t15 += v * b5; + t16 += v * b6; + t17 += v * b7; + t18 += v * b8; + t19 += v * b9; + t20 += v * b10; + t21 += v * b11; + t22 += v * b12; + t23 += v * b13; + t24 += v * b14; + t25 += v * b15; + v = a[11]; + t11 += v * b0; + t12 += v * b1; + t13 += v * b2; + t14 += v * b3; + t15 += v * b4; + t16 += v * b5; + t17 += v * b6; + t18 += v * b7; + t19 += v * b8; + t20 += v * b9; + t21 += v * b10; + t22 += v * b11; + t23 += v * b12; + t24 += v * b13; + t25 += v * b14; + t26 += v * b15; + v = a[12]; + t12 += v * b0; + t13 += v * b1; + t14 += v * b2; + t15 += v * b3; + t16 += v * b4; + t17 += v * b5; + t18 += v * b6; + t19 += v * b7; + t20 += v * b8; + t21 += v * b9; + t22 += v * b10; + t23 += v * b11; + t24 += v * b12; + t25 += v * b13; + t26 += v * b14; + t27 += v * b15; + v = a[13]; + t13 += v * b0; + t14 += v * b1; + t15 += v * b2; + t16 += v * b3; + t17 += v * b4; + t18 += v * b5; + t19 += v * b6; + t20 += v * b7; + t21 += v * b8; + t22 += v * b9; + t23 += v * b10; + t24 += v * b11; + t25 += v * b12; + t26 += v * b13; + t27 += v * b14; + t28 += v * b15; + v = a[14]; + t14 += v * b0; + t15 += v * b1; + t16 += v * b2; + t17 += v * b3; + t18 += v * b4; + t19 += v * b5; + t20 += v * b6; + t21 += v * b7; + t22 += v * b8; + t23 += v * b9; + t24 += v * b10; + t25 += v * b11; + t26 += v * b12; + t27 += v * b13; + t28 += v * b14; + t29 += v * b15; + v = a[15]; + t15 += v * b0; + t16 += v * b1; + t17 += v * b2; + t18 += v * b3; + t19 += v * b4; + t20 += v * b5; + t21 += v * b6; + t22 += v * b7; + t23 += v * b8; + t24 += v * b9; + t25 += v * b10; + t26 += v * b11; + t27 += v * b12; + t28 += v * b13; + t29 += v * b14; + t30 += v * b15; + + t0 += 38 * t16; + t1 += 38 * t17; + t2 += 38 * t18; + t3 += 38 * t19; + t4 += 38 * t20; + t5 += 38 * t21; + t6 += 38 * t22; + t7 += 38 * t23; + t8 += 38 * t24; + t9 += 38 * t25; + t10 += 38 * t26; + t11 += 38 * t27; + t12 += 38 * t28; + t13 += 38 * t29; + t14 += 38 * t30; + // t15 left as is + + // first car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + // second car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + o[ 0] = t0; + o[ 1] = t1; + o[ 2] = t2; + o[ 3] = t3; + o[ 4] = t4; + o[ 5] = t5; + o[ 6] = t6; + o[ 7] = t7; + o[ 8] = t8; + o[ 9] = t9; + o[10] = t10; + o[11] = t11; + o[12] = t12; + o[13] = t13; + o[14] = t14; + o[15] = t15; +} + +function S(o, a) { + M(o, a, a); +} + +function inv25519(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 253; a >= 0; a--) { + S(c, c); + if(a !== 2 && a !== 4) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; +} + +function pow2523(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 250; a >= 0; a--) { + S(c, c); + if(a !== 1) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; +} + +function crypto_scalarmult(q, n, p) { + var z = new Uint8Array(32); + var x = new Float64Array(80), r, i; + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(); + for (i = 0; i < 31; i++) z[i] = n[i]; + z[31]=(n[31]&127)|64; + z[0]&=248; + unpack25519(x,p); + for (i = 0; i < 16; i++) { + b[i]=x[i]; + d[i]=a[i]=c[i]=0; + } + a[0]=d[0]=1; + for (i=254; i>=0; --i) { + r=(z[i>>>3]>>>(i&7))&1; + sel25519(a,b,r); + sel25519(c,d,r); + A(e,a,c); + Z(a,a,c); + A(c,b,d); + Z(b,b,d); + S(d,e); + S(f,a); + M(a,c,a); + M(c,b,e); + A(e,a,c); + Z(a,a,c); + S(b,a); + Z(c,d,f); + M(a,c,_121665); + A(a,a,d); + M(c,c,a); + M(a,d,f); + M(d,b,x); + S(b,e); + sel25519(a,b,r); + sel25519(c,d,r); + } + for (i = 0; i < 16; i++) { + x[i+16]=a[i]; + x[i+32]=c[i]; + x[i+48]=b[i]; + x[i+64]=d[i]; + } + var x32 = x.subarray(32); + var x16 = x.subarray(16); + inv25519(x32,x32); + M(x16,x16,x32); + pack25519(q,x16); + return 0; +} + +function crypto_scalarmult_base(q, n) { + return crypto_scalarmult(q, n, _9); +} + +function crypto_box_keypair(y, x) { + randombytes(x, 32); + return crypto_scalarmult_base(y, x); +} + +function crypto_box_beforenm(k, y, x) { + var s = new Uint8Array(32); + crypto_scalarmult(s, x, y); + return crypto_core_hsalsa20(k, _0, s, sigma); +} + +var crypto_box_afternm = crypto_secretbox; +var crypto_box_open_afternm = crypto_secretbox_open; + +function crypto_box(c, m, d, n, y, x) { + var k = new Uint8Array(32); + crypto_box_beforenm(k, y, x); + return crypto_box_afternm(c, m, d, n, k); +} + +function crypto_box_open(m, c, d, n, y, x) { + var k = new Uint8Array(32); + crypto_box_beforenm(k, y, x); + return crypto_box_open_afternm(m, c, d, n, k); +} + +var K = [ + 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, + 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc, + 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, + 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, + 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe, + 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2, + 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, + 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694, + 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, + 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, + 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483, + 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5, + 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, + 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4, + 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, + 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, + 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926, + 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df, + 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, + 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b, + 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, + 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, + 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910, + 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8, + 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, + 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8, + 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, + 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, + 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60, + 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec, + 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, + 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b, + 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, + 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, + 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6, + 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b, + 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, + 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c, + 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, + 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817 +]; + +function crypto_hashblocks_hl(hh, hl, m, n) { + var wh = new Int32Array(16), wl = new Int32Array(16), + bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7, + bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7, + th, tl, i, j, h, l, a, b, c, d; + + var ah0 = hh[0], + ah1 = hh[1], + ah2 = hh[2], + ah3 = hh[3], + ah4 = hh[4], + ah5 = hh[5], + ah6 = hh[6], + ah7 = hh[7], + + al0 = hl[0], + al1 = hl[1], + al2 = hl[2], + al3 = hl[3], + al4 = hl[4], + al5 = hl[5], + al6 = hl[6], + al7 = hl[7]; + + var pos = 0; + while (n >= 128) { + for (i = 0; i < 16; i++) { + j = 8 * i + pos; + wh[i] = (m[j+0] << 24) | (m[j+1] << 16) | (m[j+2] << 8) | m[j+3]; + wl[i] = (m[j+4] << 24) | (m[j+5] << 16) | (m[j+6] << 8) | m[j+7]; + } + for (i = 0; i < 80; i++) { + bh0 = ah0; + bh1 = ah1; + bh2 = ah2; + bh3 = ah3; + bh4 = ah4; + bh5 = ah5; + bh6 = ah6; + bh7 = ah7; + + bl0 = al0; + bl1 = al1; + bl2 = al2; + bl3 = al3; + bl4 = al4; + bl5 = al5; + bl6 = al6; + bl7 = al7; + + // add + h = ah7; + l = al7; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + // Sigma1 + h = ((ah4 >>> 14) | (al4 << (32-14))) ^ ((ah4 >>> 18) | (al4 << (32-18))) ^ ((al4 >>> (41-32)) | (ah4 << (32-(41-32)))); + l = ((al4 >>> 14) | (ah4 << (32-14))) ^ ((al4 >>> 18) | (ah4 << (32-18))) ^ ((ah4 >>> (41-32)) | (al4 << (32-(41-32)))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // Ch + h = (ah4 & ah5) ^ (~ah4 & ah6); + l = (al4 & al5) ^ (~al4 & al6); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // K + h = K[i*2]; + l = K[i*2+1]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // w + h = wh[i%16]; + l = wl[i%16]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + th = c & 0xffff | d << 16; + tl = a & 0xffff | b << 16; + + // add + h = th; + l = tl; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + // Sigma0 + h = ((ah0 >>> 28) | (al0 << (32-28))) ^ ((al0 >>> (34-32)) | (ah0 << (32-(34-32)))) ^ ((al0 >>> (39-32)) | (ah0 << (32-(39-32)))); + l = ((al0 >>> 28) | (ah0 << (32-28))) ^ ((ah0 >>> (34-32)) | (al0 << (32-(34-32)))) ^ ((ah0 >>> (39-32)) | (al0 << (32-(39-32)))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // Maj + h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2); + l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + bh7 = (c & 0xffff) | (d << 16); + bl7 = (a & 0xffff) | (b << 16); + + // add + h = bh3; + l = bl3; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = th; + l = tl; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + bh3 = (c & 0xffff) | (d << 16); + bl3 = (a & 0xffff) | (b << 16); + + ah1 = bh0; + ah2 = bh1; + ah3 = bh2; + ah4 = bh3; + ah5 = bh4; + ah6 = bh5; + ah7 = bh6; + ah0 = bh7; + + al1 = bl0; + al2 = bl1; + al3 = bl2; + al4 = bl3; + al5 = bl4; + al6 = bl5; + al7 = bl6; + al0 = bl7; + + if (i%16 === 15) { + for (j = 0; j < 16; j++) { + // add + h = wh[j]; + l = wl[j]; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = wh[(j+9)%16]; + l = wl[(j+9)%16]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // sigma0 + th = wh[(j+1)%16]; + tl = wl[(j+1)%16]; + h = ((th >>> 1) | (tl << (32-1))) ^ ((th >>> 8) | (tl << (32-8))) ^ (th >>> 7); + l = ((tl >>> 1) | (th << (32-1))) ^ ((tl >>> 8) | (th << (32-8))) ^ ((tl >>> 7) | (th << (32-7))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // sigma1 + th = wh[(j+14)%16]; + tl = wl[(j+14)%16]; + h = ((th >>> 19) | (tl << (32-19))) ^ ((tl >>> (61-32)) | (th << (32-(61-32)))) ^ (th >>> 6); + l = ((tl >>> 19) | (th << (32-19))) ^ ((th >>> (61-32)) | (tl << (32-(61-32)))) ^ ((tl >>> 6) | (th << (32-6))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + wh[j] = (c & 0xffff) | (d << 16); + wl[j] = (a & 0xffff) | (b << 16); + } + } + } + + // add + h = ah0; + l = al0; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[0]; + l = hl[0]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[0] = ah0 = (c & 0xffff) | (d << 16); + hl[0] = al0 = (a & 0xffff) | (b << 16); + + h = ah1; + l = al1; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[1]; + l = hl[1]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[1] = ah1 = (c & 0xffff) | (d << 16); + hl[1] = al1 = (a & 0xffff) | (b << 16); + + h = ah2; + l = al2; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[2]; + l = hl[2]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[2] = ah2 = (c & 0xffff) | (d << 16); + hl[2] = al2 = (a & 0xffff) | (b << 16); + + h = ah3; + l = al3; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[3]; + l = hl[3]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[3] = ah3 = (c & 0xffff) | (d << 16); + hl[3] = al3 = (a & 0xffff) | (b << 16); + + h = ah4; + l = al4; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[4]; + l = hl[4]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[4] = ah4 = (c & 0xffff) | (d << 16); + hl[4] = al4 = (a & 0xffff) | (b << 16); + + h = ah5; + l = al5; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[5]; + l = hl[5]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[5] = ah5 = (c & 0xffff) | (d << 16); + hl[5] = al5 = (a & 0xffff) | (b << 16); + + h = ah6; + l = al6; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[6]; + l = hl[6]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[6] = ah6 = (c & 0xffff) | (d << 16); + hl[6] = al6 = (a & 0xffff) | (b << 16); + + h = ah7; + l = al7; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[7]; + l = hl[7]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[7] = ah7 = (c & 0xffff) | (d << 16); + hl[7] = al7 = (a & 0xffff) | (b << 16); + + pos += 128; + n -= 128; + } + + return n; +} + +function crypto_hash(out, m, n) { + var hh = new Int32Array(8), + hl = new Int32Array(8), + x = new Uint8Array(256), + i, b = n; + + hh[0] = 0x6a09e667; + hh[1] = 0xbb67ae85; + hh[2] = 0x3c6ef372; + hh[3] = 0xa54ff53a; + hh[4] = 0x510e527f; + hh[5] = 0x9b05688c; + hh[6] = 0x1f83d9ab; + hh[7] = 0x5be0cd19; + + hl[0] = 0xf3bcc908; + hl[1] = 0x84caa73b; + hl[2] = 0xfe94f82b; + hl[3] = 0x5f1d36f1; + hl[4] = 0xade682d1; + hl[5] = 0x2b3e6c1f; + hl[6] = 0xfb41bd6b; + hl[7] = 0x137e2179; + + crypto_hashblocks_hl(hh, hl, m, n); + n %= 128; + + for (i = 0; i < n; i++) x[i] = m[b-n+i]; + x[n] = 128; + + n = 256-128*(n<112?1:0); + x[n-9] = 0; + ts64(x, n-8, (b / 0x20000000) | 0, b << 3); + crypto_hashblocks_hl(hh, hl, x, n); + + for (i = 0; i < 8; i++) ts64(out, 8*i, hh[i], hl[i]); + + return 0; +} + +function add(p, q) { + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(), + g = gf(), h = gf(), t = gf(); + + Z(a, p[1], p[0]); + Z(t, q[1], q[0]); + M(a, a, t); + A(b, p[0], p[1]); + A(t, q[0], q[1]); + M(b, b, t); + M(c, p[3], q[3]); + M(c, c, D2); + M(d, p[2], q[2]); + A(d, d, d); + Z(e, b, a); + Z(f, d, c); + A(g, d, c); + A(h, b, a); + + M(p[0], e, f); + M(p[1], h, g); + M(p[2], g, f); + M(p[3], e, h); +} + +function cswap(p, q, b) { + var i; + for (i = 0; i < 4; i++) { + sel25519(p[i], q[i], b); + } +} + +function pack(r, p) { + var tx = gf(), ty = gf(), zi = gf(); + inv25519(zi, p[2]); + M(tx, p[0], zi); + M(ty, p[1], zi); + pack25519(r, ty); + r[31] ^= par25519(tx) << 7; +} + +function scalarmult(p, q, s) { + var b, i; + set25519(p[0], gf0); + set25519(p[1], gf1); + set25519(p[2], gf1); + set25519(p[3], gf0); + for (i = 255; i >= 0; --i) { + b = (s[(i/8)|0] >> (i&7)) & 1; + cswap(p, q, b); + add(q, p); + add(p, p); + cswap(p, q, b); + } +} + +function scalarbase(p, s) { + var q = [gf(), gf(), gf(), gf()]; + set25519(q[0], X); + set25519(q[1], Y); + set25519(q[2], gf1); + M(q[3], X, Y); + scalarmult(p, q, s); +} + +function crypto_sign_keypair(pk, sk, seeded) { + var d = new Uint8Array(64); + var p = [gf(), gf(), gf(), gf()]; + var i; + + if (!seeded) randombytes(sk, 32); + crypto_hash(d, sk, 32); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + scalarbase(p, d); + pack(pk, p); + + for (i = 0; i < 32; i++) sk[i+32] = pk[i]; + return 0; +} + +var L = new Float64Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]); + +function modL(r, x) { + var carry, i, j, k; + for (i = 63; i >= 32; --i) { + carry = 0; + for (j = i - 32, k = i - 12; j < k; ++j) { + x[j] += carry - 16 * x[i] * L[j - (i - 32)]; + carry = Math.floor((x[j] + 128) / 256); + x[j] -= carry * 256; + } + x[j] += carry; + x[i] = 0; + } + carry = 0; + for (j = 0; j < 32; j++) { + x[j] += carry - (x[31] >> 4) * L[j]; + carry = x[j] >> 8; + x[j] &= 255; + } + for (j = 0; j < 32; j++) x[j] -= carry * L[j]; + for (i = 0; i < 32; i++) { + x[i+1] += x[i] >> 8; + r[i] = x[i] & 255; + } +} + +function reduce(r) { + var x = new Float64Array(64), i; + for (i = 0; i < 64; i++) x[i] = r[i]; + for (i = 0; i < 64; i++) r[i] = 0; + modL(r, x); +} + +// Note: difference from C - smlen returned, not passed as argument. +function crypto_sign(sm, m, n, sk) { + var d = new Uint8Array(64), h = new Uint8Array(64), r = new Uint8Array(64); + var i, j, x = new Float64Array(64); + var p = [gf(), gf(), gf(), gf()]; + + crypto_hash(d, sk, 32); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + var smlen = n + 64; + for (i = 0; i < n; i++) sm[64 + i] = m[i]; + for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i]; + + crypto_hash(r, sm.subarray(32), n+32); + reduce(r); + scalarbase(p, r); + pack(sm, p); + + for (i = 32; i < 64; i++) sm[i] = sk[i]; + crypto_hash(h, sm, n + 64); + reduce(h); + + for (i = 0; i < 64; i++) x[i] = 0; + for (i = 0; i < 32; i++) x[i] = r[i]; + for (i = 0; i < 32; i++) { + for (j = 0; j < 32; j++) { + x[i+j] += h[i] * d[j]; + } + } + + modL(sm.subarray(32), x); + return smlen; +} + +function unpackneg(r, p) { + var t = gf(), chk = gf(), num = gf(), + den = gf(), den2 = gf(), den4 = gf(), + den6 = gf(); + + set25519(r[2], gf1); + unpack25519(r[1], p); + S(num, r[1]); + M(den, num, D); + Z(num, num, r[2]); + A(den, r[2], den); + + S(den2, den); + S(den4, den2); + M(den6, den4, den2); + M(t, den6, num); + M(t, t, den); + + pow2523(t, t); + M(t, t, num); + M(t, t, den); + M(t, t, den); + M(r[0], t, den); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) M(r[0], r[0], I); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) return -1; + + if (par25519(r[0]) === (p[31]>>7)) Z(r[0], gf0, r[0]); + + M(r[3], r[0], r[1]); + return 0; +} + +function crypto_sign_open(m, sm, n, pk) { + var i; + var t = new Uint8Array(32), h = new Uint8Array(64); + var p = [gf(), gf(), gf(), gf()], + q = [gf(), gf(), gf(), gf()]; + + if (n < 64) return -1; + + if (unpackneg(q, pk)) return -1; + + for (i = 0; i < n; i++) m[i] = sm[i]; + for (i = 0; i < 32; i++) m[i+32] = pk[i]; + crypto_hash(h, m, n); + reduce(h); + scalarmult(p, q, h); + + scalarbase(q, sm.subarray(32)); + add(p, q); + pack(t, p); + + n -= 64; + if (crypto_verify_32(sm, 0, t, 0)) { + for (i = 0; i < n; i++) m[i] = 0; + return -1; + } + + for (i = 0; i < n; i++) m[i] = sm[i + 64]; + return n; +} + +var crypto_secretbox_KEYBYTES = 32, + crypto_secretbox_NONCEBYTES = 24, + crypto_secretbox_ZEROBYTES = 32, + crypto_secretbox_BOXZEROBYTES = 16, + crypto_scalarmult_BYTES = 32, + crypto_scalarmult_SCALARBYTES = 32, + crypto_box_PUBLICKEYBYTES = 32, + crypto_box_SECRETKEYBYTES = 32, + crypto_box_BEFORENMBYTES = 32, + crypto_box_NONCEBYTES = crypto_secretbox_NONCEBYTES, + crypto_box_ZEROBYTES = crypto_secretbox_ZEROBYTES, + crypto_box_BOXZEROBYTES = crypto_secretbox_BOXZEROBYTES, + crypto_sign_BYTES = 64, + crypto_sign_PUBLICKEYBYTES = 32, + crypto_sign_SECRETKEYBYTES = 64, + crypto_sign_SEEDBYTES = 32, + crypto_hash_BYTES = 64; + +nacl.lowlevel = { + crypto_core_hsalsa20: crypto_core_hsalsa20, + crypto_stream_xor: crypto_stream_xor, + crypto_stream: crypto_stream, + crypto_stream_salsa20_xor: crypto_stream_salsa20_xor, + crypto_stream_salsa20: crypto_stream_salsa20, + crypto_onetimeauth: crypto_onetimeauth, + crypto_onetimeauth_verify: crypto_onetimeauth_verify, + crypto_verify_16: crypto_verify_16, + crypto_verify_32: crypto_verify_32, + crypto_secretbox: crypto_secretbox, + crypto_secretbox_open: crypto_secretbox_open, + crypto_scalarmult: crypto_scalarmult, + crypto_scalarmult_base: crypto_scalarmult_base, + crypto_box_beforenm: crypto_box_beforenm, + crypto_box_afternm: crypto_box_afternm, + crypto_box: crypto_box, + crypto_box_open: crypto_box_open, + crypto_box_keypair: crypto_box_keypair, + crypto_hash: crypto_hash, + crypto_sign: crypto_sign, + crypto_sign_keypair: crypto_sign_keypair, + crypto_sign_open: crypto_sign_open, + + crypto_secretbox_KEYBYTES: crypto_secretbox_KEYBYTES, + crypto_secretbox_NONCEBYTES: crypto_secretbox_NONCEBYTES, + crypto_secretbox_ZEROBYTES: crypto_secretbox_ZEROBYTES, + crypto_secretbox_BOXZEROBYTES: crypto_secretbox_BOXZEROBYTES, + crypto_scalarmult_BYTES: crypto_scalarmult_BYTES, + crypto_scalarmult_SCALARBYTES: crypto_scalarmult_SCALARBYTES, + crypto_box_PUBLICKEYBYTES: crypto_box_PUBLICKEYBYTES, + crypto_box_SECRETKEYBYTES: crypto_box_SECRETKEYBYTES, + crypto_box_BEFORENMBYTES: crypto_box_BEFORENMBYTES, + crypto_box_NONCEBYTES: crypto_box_NONCEBYTES, + crypto_box_ZEROBYTES: crypto_box_ZEROBYTES, + crypto_box_BOXZEROBYTES: crypto_box_BOXZEROBYTES, + crypto_sign_BYTES: crypto_sign_BYTES, + crypto_sign_PUBLICKEYBYTES: crypto_sign_PUBLICKEYBYTES, + crypto_sign_SECRETKEYBYTES: crypto_sign_SECRETKEYBYTES, + crypto_sign_SEEDBYTES: crypto_sign_SEEDBYTES, + crypto_hash_BYTES: crypto_hash_BYTES, + + gf: gf, + D: D, + L: L, + pack25519: pack25519, + unpack25519: unpack25519, + M: M, + A: A, + S: S, + Z: Z, + pow2523: pow2523, + add: add, + set25519: set25519, + modL: modL, + scalarmult: scalarmult, + scalarbase: scalarbase, +}; + +/* High-level API */ + +function checkLengths(k, n) { + if (k.length !== crypto_secretbox_KEYBYTES) throw new Error('bad key size'); + if (n.length !== crypto_secretbox_NONCEBYTES) throw new Error('bad nonce size'); +} + +function checkBoxLengths(pk, sk) { + if (pk.length !== crypto_box_PUBLICKEYBYTES) throw new Error('bad public key size'); + if (sk.length !== crypto_box_SECRETKEYBYTES) throw new Error('bad secret key size'); +} + +function checkArrayTypes() { + for (var i = 0; i < arguments.length; i++) { + if (!(arguments[i] instanceof Uint8Array)) + throw new TypeError('unexpected type, use Uint8Array'); + } +} + +function cleanup(arr) { + for (var i = 0; i < arr.length; i++) arr[i] = 0; +} + +nacl.randomBytes = function(n) { + var b = new Uint8Array(n); + randombytes(b, n); + return b; +}; + +nacl.secretbox = function(msg, nonce, key) { + checkArrayTypes(msg, nonce, key); + checkLengths(key, nonce); + var m = new Uint8Array(crypto_secretbox_ZEROBYTES + msg.length); + var c = new Uint8Array(m.length); + for (var i = 0; i < msg.length; i++) m[i+crypto_secretbox_ZEROBYTES] = msg[i]; + crypto_secretbox(c, m, m.length, nonce, key); + return c.subarray(crypto_secretbox_BOXZEROBYTES); +}; + +nacl.secretbox.open = function(box, nonce, key) { + checkArrayTypes(box, nonce, key); + checkLengths(key, nonce); + var c = new Uint8Array(crypto_secretbox_BOXZEROBYTES + box.length); + var m = new Uint8Array(c.length); + for (var i = 0; i < box.length; i++) c[i+crypto_secretbox_BOXZEROBYTES] = box[i]; + if (c.length < 32) return null; + if (crypto_secretbox_open(m, c, c.length, nonce, key) !== 0) return null; + return m.subarray(crypto_secretbox_ZEROBYTES); +}; + +nacl.secretbox.keyLength = crypto_secretbox_KEYBYTES; +nacl.secretbox.nonceLength = crypto_secretbox_NONCEBYTES; +nacl.secretbox.overheadLength = crypto_secretbox_BOXZEROBYTES; + +nacl.scalarMult = function(n, p) { + checkArrayTypes(n, p); + if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); + if (p.length !== crypto_scalarmult_BYTES) throw new Error('bad p size'); + var q = new Uint8Array(crypto_scalarmult_BYTES); + crypto_scalarmult(q, n, p); + return q; +}; + +nacl.scalarMult.base = function(n) { + checkArrayTypes(n); + if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); + var q = new Uint8Array(crypto_scalarmult_BYTES); + crypto_scalarmult_base(q, n); + return q; +}; + +nacl.scalarMult.scalarLength = crypto_scalarmult_SCALARBYTES; +nacl.scalarMult.groupElementLength = crypto_scalarmult_BYTES; + +nacl.box = function(msg, nonce, publicKey, secretKey) { + var k = nacl.box.before(publicKey, secretKey); + return nacl.secretbox(msg, nonce, k); +}; + +nacl.box.before = function(publicKey, secretKey) { + checkArrayTypes(publicKey, secretKey); + checkBoxLengths(publicKey, secretKey); + var k = new Uint8Array(crypto_box_BEFORENMBYTES); + crypto_box_beforenm(k, publicKey, secretKey); + return k; +}; + +nacl.box.after = nacl.secretbox; + +nacl.box.open = function(msg, nonce, publicKey, secretKey) { + var k = nacl.box.before(publicKey, secretKey); + return nacl.secretbox.open(msg, nonce, k); +}; + +nacl.box.open.after = nacl.secretbox.open; + +nacl.box.keyPair = function() { + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_box_SECRETKEYBYTES); + crypto_box_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.box.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_box_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + crypto_scalarmult_base(pk, secretKey); + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; +}; + +nacl.box.publicKeyLength = crypto_box_PUBLICKEYBYTES; +nacl.box.secretKeyLength = crypto_box_SECRETKEYBYTES; +nacl.box.sharedKeyLength = crypto_box_BEFORENMBYTES; +nacl.box.nonceLength = crypto_box_NONCEBYTES; +nacl.box.overheadLength = nacl.secretbox.overheadLength; + +nacl.sign = function(msg, secretKey) { + checkArrayTypes(msg, secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var signedMsg = new Uint8Array(crypto_sign_BYTES+msg.length); + crypto_sign(signedMsg, msg, msg.length, secretKey); + return signedMsg; +}; + +nacl.sign.open = function(signedMsg, publicKey) { + checkArrayTypes(signedMsg, publicKey); + if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) + throw new Error('bad public key size'); + var tmp = new Uint8Array(signedMsg.length); + var mlen = crypto_sign_open(tmp, signedMsg, signedMsg.length, publicKey); + if (mlen < 0) return null; + var m = new Uint8Array(mlen); + for (var i = 0; i < m.length; i++) m[i] = tmp[i]; + return m; +}; + +nacl.sign.detached = function(msg, secretKey) { + var signedMsg = nacl.sign(msg, secretKey); + var sig = new Uint8Array(crypto_sign_BYTES); + for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; + return sig; +}; + +nacl.sign.detached.verify = function(msg, sig, publicKey) { + checkArrayTypes(msg, sig, publicKey); + if (sig.length !== crypto_sign_BYTES) + throw new Error('bad signature size'); + if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) + throw new Error('bad public key size'); + var sm = new Uint8Array(crypto_sign_BYTES + msg.length); + var m = new Uint8Array(crypto_sign_BYTES + msg.length); + var i; + for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i]; + for (i = 0; i < msg.length; i++) sm[i+crypto_sign_BYTES] = msg[i]; + return (crypto_sign_open(m, sm, sm.length, publicKey) >= 0); +}; + +nacl.sign.keyPair = function() { + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + crypto_sign_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.sign.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + for (var i = 0; i < pk.length; i++) pk[i] = secretKey[32+i]; + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; +}; + +nacl.sign.keyPair.fromSeed = function(seed) { + checkArrayTypes(seed); + if (seed.length !== crypto_sign_SEEDBYTES) + throw new Error('bad seed size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + for (var i = 0; i < 32; i++) sk[i] = seed[i]; + crypto_sign_keypair(pk, sk, true); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.sign.publicKeyLength = crypto_sign_PUBLICKEYBYTES; +nacl.sign.secretKeyLength = crypto_sign_SECRETKEYBYTES; +nacl.sign.seedLength = crypto_sign_SEEDBYTES; +nacl.sign.signatureLength = crypto_sign_BYTES; + +nacl.hash = function(msg) { + checkArrayTypes(msg); + var h = new Uint8Array(crypto_hash_BYTES); + crypto_hash(h, msg, msg.length); + return h; +}; + +nacl.hash.hashLength = crypto_hash_BYTES; + +nacl.verify = function(x, y) { + checkArrayTypes(x, y); + // Zero length arguments are considered not equal. + if (x.length === 0 || y.length === 0) return false; + if (x.length !== y.length) return false; + return (vn(x, 0, y, 0, x.length) === 0) ? true : false; +}; + +nacl.setPRNG = function(fn) { + randombytes = fn; +}; + +(function() { + // Initialize PRNG if environment provides CSPRNG. + // If not, methods calling randombytes will throw. + var crypto = typeof self !== 'undefined' ? (self.crypto || self.msCrypto) : null; + if (crypto && crypto.getRandomValues) { + // Browsers. + var QUOTA = 65536; + nacl.setPRNG(function(x, n) { + var i, v = new Uint8Array(n); + for (i = 0; i < n; i += QUOTA) { + crypto.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA))); + } + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } else if (true) { + // Node.js. + crypto = __nccwpck_require__(6113); + if (crypto && crypto.randomBytes) { + nacl.setPRNG(function(x, n) { + var i, v = crypto.randomBytes(n); + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } + } +})(); + +})( true && module.exports ? module.exports : (self.nacl = self.nacl || {})); + + /***/ }), /***/ 25716: @@ -50945,6 +53343,371 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 56637: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SETUP_FEATURE_DESCRIPTIONS = exports.SETUP_AGENT_TASKS = void 0; +exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; +exports.mergeSetupConfiguration = mergeSetupConfiguration; +exports.validateSetupConfiguration = validateSetupConfiguration; +exports.buildSetupPlan = buildSetupPlan; +exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; +exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; +exports.buildSetupActionInputs = buildSetupActionInputs; +const agent_1 = __nccwpck_require__(89040); +const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); +exports.SETUP_AGENT_TASKS = [ + 'planner', + 'findings', + 'reviewer', + 'fixer', + 'tester', + 'release', +]; +exports.SETUP_FEATURE_DESCRIPTIONS = { + issues: 'Issue automation: branching, labels, projects, and issue lifecycle', + pullRequests: 'Pull request automation: review, descriptions, and lifecycle', + commits: 'Commit automation: progress, sizing, and Bugbot analysis', + issueComments: 'Issue comments: questions, translations, and Bugbot autofix', + pullRequestComments: 'Pull request review comments: translations and Bugbot autofix', + release: 'Release workflow: versioning, changelog, tag, and GitHub Release', + hotfix: 'Hotfix workflow: emergency release from a production tag', + agentProvisioning: 'Agent CLI provisioning check workflow', + credentialHealth: 'Read-only remote credential health workflow for setup and doctor', + issueTemplates: 'Issue templates for feature, bug, documentation, and operations', + pullRequestTemplate: 'Pull request template', +}; +const WORKFLOW_FILES = { + issues: ['copilot_issue.yml'], + pullRequests: ['copilot_pull_request.yml'], + commits: ['copilot_commit.yml'], + issueComments: ['copilot_issue_comment.yml'], + pullRequestComments: ['copilot_pull_request_comment.yml'], + release: ['release_workflow.yml'], + hotfix: ['hotfix_workflow.yml'], + agentProvisioning: ['agent-cli-provisioning.yml'], + credentialHealth: ['copilot_credential_health.yml'], +}; +const ISSUE_TEMPLATE_FILES = [ + 'config.yml', + 'feature_request.yml', + 'bug_report.yml', + 'doc_update.yml', + 'chore_task.yml', + 'help_request.yml', + 'hotfix.yml', + 'release.yml', +]; +const SECRET_BY_MODEL_PROVIDER = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + google: 'GOOGLE_API_KEY', + openrouter: 'OPENROUTER_API_KEY', +}; +function createDefaultSetupConfiguration() { + const defaultRole = () => ({ + provider: agent_1.DEFAULT_AGENT_PROVIDER, + modelProvider: agent_1.DEFAULT_MODEL_PROVIDER, + model: agent_1.DEFAULT_AGENT_MODEL, + effort: '', + }); + const agents = Object.fromEntries(exports.SETUP_AGENT_TASKS.map(task => [task, defaultRole()])); + const features = Object.fromEntries(Object.keys(exports.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true])); + return { + features, + agents, + repository: { + mainBranch: 'master', + developmentBranch: 'develop', + featureTree: 'feature', + bugfixTree: 'bugfix', + hotfixTree: 'hotfix', + releaseTree: 'release', + docsTree: 'docs', + choreTree: 'chore', + branchManagementAlways: false, + reopenIssueOnPush: true, + desiredAssigneesCount: 1, + desiredReviewersCount: 1, + mergeTimeout: 600, + issueLocale: 'en-US', + pullRequestLocale: 'en-US', + commitPrefixTransforms: 'replace-slash', + }, + ai: { + pullRequestDescription: true, + ignoreFiles: 'build/*', + membersOnly: false, + includeReasoning: true, + bugbotSeverity: 'low', + bugbotCommentLimit: 20, + bugbotFixVerifyCommands: '', + provisioningMode: 'auto', + }, + projects: { + ids: '', + issueCreatedColumn: 'Todo', + pullRequestCreatedColumn: 'In Progress', + issueInProgressColumn: 'In Progress', + pullRequestInProgressColumn: 'In Progress', + }, + createInitialTag: true, + manageRepositoryVariables: true, + manageRepositorySecrets: true, + actionInputs: {}, + }; +} +function mergeSetupConfiguration(base, overrides = {}) { + const agents = { ...base.agents }; + for (const task of exports.SETUP_AGENT_TASKS) { + agents[task] = { ...base.agents[task], ...(overrides.agents?.[task] ?? {}) }; + } + return { + ...base, + features: { ...base.features, ...(overrides.features ?? {}) }, + agents, + repository: { ...base.repository, ...(overrides.repository ?? {}) }, + ai: { ...base.ai, ...(overrides.ai ?? {}) }, + projects: { ...base.projects, ...(overrides.projects ?? {}) }, + createInitialTag: overrides.createInitialTag ?? base.createInitialTag, + manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, + manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, + actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, + }; +} +function validateSetupConfiguration(configuration) { + const errors = []; + const nonEmpty = [ + ['main branch', configuration.repository.mainBranch], + ['development branch', configuration.repository.developmentBranch], + ['feature branch prefix', configuration.repository.featureTree], + ['bugfix branch prefix', configuration.repository.bugfixTree], + ['hotfix branch prefix', configuration.repository.hotfixTree], + ['release branch prefix', configuration.repository.releaseTree], + ['docs branch prefix', configuration.repository.docsTree], + ['chore branch prefix', configuration.repository.choreTree], + ]; + for (const [name, value] of nonEmpty) { + if (!value.trim() || /\s/.test(value)) + errors.push(`The ${name} must be non-empty and contain no whitespace.`); + } + if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { + errors.push('Desired assignees must be between 0 and 10.'); + } + if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { + errors.push('Desired reviewers must be between 0 and 15.'); + } + if (configuration.repository.mergeTimeout < 0) + errors.push('Merge timeout cannot be negative.'); + if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { + errors.push('Bugbot comment limit must be between 1 and 100.'); + } + if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { + errors.push('Bugbot severity must be info, low, medium, or high.'); + } + if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { + errors.push('Agent provisioning must be auto, always, or disabled.'); + } + for (const task of exports.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) + errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); + if (!agent.modelProvider.trim() || !agent.model.trim()) + errors.push(`Model provider and model are required for ${task}.`); + if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) + errors.push(`Model provider and model for ${task} cannot contain whitespace.`); + } + return errors; +} +function buildSetupPlan(configuration) { + const workflowFiles = Object.entries(WORKFLOW_FILES) + .filter(([feature]) => configuration.features[feature] !== false) + .flatMap(([, files]) => files); + const issueTemplateFiles = configuration.features.issueTemplates === false + ? [] + : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') + .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); + const selectedFiles = [ + ...workflowFiles.map(file => `workflows/${file}`), + ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), + ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), + ]; + return { + configuration, + workflowFiles, + issueTemplateFiles, + selectedFiles, + variables: buildSetupRepositoryVariables(configuration), + requiredSecrets: buildRequiredSetupSecrets(configuration), + credentialRequirements: buildSetupCredentialRequirements(configuration), + warnings: buildSetupWarnings(configuration), + }; +} +/** Builds the non-sensitive credential contract implied by the selected agents. */ +function buildSetupCredentialRequirements(configuration) { + const requirements = new Map(); + const add = (name, kind, description, provider, model) => { + if (!requirements.has(name)) + requirements.set(name, { name, kind, description, provider, model }); + }; + add('PAT', 'workflowPat', 'A separate GitHub token owned by the bot account. It is used by workflows at runtime.'); + for (const task of exports.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (agent.provider === 'cursor') { + add('CURSOR_API_KEY', 'apiKey', 'Cursor API key used by the Cursor agent runtime.', 'cursor', agent.model); + continue; + } + if (agent.provider === 'opencode') + add('OPENCODE_API_KEY', 'apiKey', 'OpenCode API key used by the OpenCode agent runtime.', 'opencode', agent.model); + if (agent.provider === 'codex') + add('CODEX_ACCESS_TOKEN', 'apiKey', 'Codex access token used by the Codex agent runtime.', 'codex', agent.model); + const modelProvider = agent.modelProvider.trim().toLowerCase(); + if (modelProvider && !['local', 'ollama', 'lmstudio'].includes(modelProvider)) { + const name = SECRET_BY_MODEL_PROVIDER[modelProvider] ?? `${modelProvider.replace(/-/g, '_').toUpperCase()}_API_KEY`; + add(name, 'apiKey', `${modelProvider} API key for ${agent.model}.`, modelProvider, agent.model); + } + } + return [...requirements.values()]; +} +function buildSetupRepositoryVariables(configuration) { + const variables = []; + const add = (name, value) => { + if (value === undefined || value === '') + return; + variables.push({ name, value: String(value) }); + }; + const base = configuration.agents.findings; + add('AGENT_PROVIDER', base.provider); + add('AGENT_MODEL_PROVIDER', base.modelProvider); + add('AGENT_MODEL', base.model); + add('AGENT_EFFORT', base.effort); + add('AGENT_PROVISIONING', configuration.ai.provisioningMode); + add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(exports.SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); + add('AGENT_ALLOWED_MODELS', unique(exports.SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); + for (const task of exports.SETUP_AGENT_TASKS) { + const prefix = task.toUpperCase(); + const agent = configuration.agents[task]; + add(`${prefix}_PROVIDER`, agent.provider); + add(`${prefix}_MODEL_PROVIDER`, agent.modelProvider); + add(`${prefix}_MODEL`, agent.model); + add(`${prefix}_EFFORT`, agent.effort); + } + const repository = configuration.repository; + add('MAIN_BRANCH', repository.mainBranch); + add('DEVELOPMENT_BRANCH', repository.developmentBranch); + add('FEATURE_TREE', repository.featureTree); + add('BUGFIX_TREE', repository.bugfixTree); + add('HOTFIX_TREE', repository.hotfixTree); + add('RELEASE_TREE', repository.releaseTree); + add('DOCS_TREE', repository.docsTree); + add('CHORE_TREE', repository.choreTree); + add('BRANCH_MANAGEMENT_ALWAYS', repository.branchManagementAlways); + add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); + add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); + add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); + add('MERGE_TIMEOUT', repository.mergeTimeout); + add('ISSUES_LOCALE', repository.issueLocale); + add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); + add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); + add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); + add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); + add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); + add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); + add('BUGBOT_SEVERITY', configuration.ai.bugbotSeverity); + add('BUGBOT_COMMENT_LIMIT', configuration.ai.bugbotCommentLimit); + add('BUGBOT_AUTOFIX_VERIFY_COMMANDS', configuration.ai.bugbotFixVerifyCommands); + add('PROJECT_IDS', configuration.projects.ids); + add('PROJECT_COLUMN_ISSUE_CREATED', configuration.projects.issueCreatedColumn); + add('PROJECT_COLUMN_PULL_REQUEST_CREATED', configuration.projects.pullRequestCreatedColumn); + add('PROJECT_COLUMN_ISSUE_IN_PROGRESS', configuration.projects.issueInProgressColumn); + add('PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS', configuration.projects.pullRequestInProgressColumn); + return variables; +} +function buildSetupActionInputs(configuration) { + const repository = configuration.repository; + const ai = configuration.ai; + const projects = configuration.projects; + return { + 'main-branch': repository.mainBranch, + 'development-branch': repository.developmentBranch, + 'feature-tree': repository.featureTree, + 'bugfix-tree': repository.bugfixTree, + 'hotfix-tree': repository.hotfixTree, + 'release-tree': repository.releaseTree, + 'docs-tree': repository.docsTree, + 'chore-tree': repository.choreTree, + 'branch-management-always': String(repository.branchManagementAlways), + 'reopen-issue-on-push': String(repository.reopenIssueOnPush), + 'desired-assignees-count': String(repository.desiredAssigneesCount), + 'desired-reviewers-count': String(repository.desiredReviewersCount), + 'merge-timeout': String(repository.mergeTimeout), + 'issues-locale': repository.issueLocale, + 'pull-requests-locale': repository.pullRequestLocale, + 'commit-prefix-transforms': repository.commitPrefixTransforms, + 'ai-pull-request-description': String(ai.pullRequestDescription), + 'ai-ignore-files': ai.ignoreFiles, + 'ai-members-only': String(ai.membersOnly), + 'ai-include-reasoning': String(ai.includeReasoning), + 'bugbot-severity': ai.bugbotSeverity, + 'bugbot-comment-limit': String(ai.bugbotCommentLimit), + 'bugbot-fix-verify-commands': ai.bugbotFixVerifyCommands, + 'project-ids': projects.ids, + 'project-column-issue-created': projects.issueCreatedColumn, + 'project-column-pull-request-created': projects.pullRequestCreatedColumn, + 'project-column-issue-in-progress': projects.issueInProgressColumn, + 'project-column-pull-request-in-progress': projects.pullRequestInProgressColumn, + ...buildAgentActionInputs(configuration), + ...configuration.actionInputs, + }; +} +function buildAgentActionInputs(configuration) { + const result = {}; + const base = configuration.agents.findings; + const add = (key, value) => { if (value !== undefined) + result[key] = value; }; + add('agent-provider', base.provider); + add('agent-model-provider', base.modelProvider); + add('agent-model', base.model); + add('agent-effort', base.effort); + for (const task of exports.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + const prefix = `${task}-`; + add(`${prefix}provider`, agent.provider); + add(`${prefix}model-provider`, agent.modelProvider); + add(`${prefix}model`, agent.model); + add(`${prefix}effort`, agent.effort); + } + return result; +} +function buildRequiredSetupSecrets(configuration) { + return buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); +} +function buildSetupWarnings(configuration) { + const warnings = []; + if (configuration.features.release !== false && configuration.features.hotfix !== false) { + warnings.push('Release and hotfix workflows require the repository secret PAT and a writable token.'); + } + if (configuration.ai.provisioningMode === 'always') { + warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); + } + if (configuration.projects.ids.trim()) { + warnings.push('Project IDs must be accessible to the PAT and use the expected project column names.'); + } + if (exports.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { + warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); + } + return warnings; +} +function unique(values) { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} + + /***/ }), /***/ 43193: @@ -51593,7 +54356,7 @@ exports.InitialSetupUseCase = void 0; const initial_setup_workflow_1 = __nccwpck_require__(18079); /** Application boundary for provisioning a repository for Copilot automation. */ class InitialSetupUseCase { - constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort) { + constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort) { this.authenticatedUserPort = authenticatedUserPort; this.initialLabelProvisioningPort = initialLabelProvisioningPort; this.issueTypeProvisioningPort = issueTypeProvisioningPort; @@ -51601,6 +54364,8 @@ class InitialSetupUseCase { this.repositoryDefaultBranchPort = repositoryDefaultBranchPort; this.repositoryTagPort = repositoryTagPort; this.setupWorkspacePort = setupWorkspacePort; + this.setupRepositoryVariablesPort = setupRepositoryVariablesPort; + this.setupRepositorySecretsPort = setupRepositorySecretsPort; this.taskId = 'InitialSetupUseCase'; } async invoke(param) { @@ -51612,6 +54377,8 @@ class InitialSetupUseCase { repositoryDefaultBranchPort: this.repositoryDefaultBranchPort, repositoryTagPort: this.repositoryTagPort, setupWorkspacePort: this.setupWorkspacePort, + setupRepositoryVariablesPort: this.setupRepositoryVariablesPort, + setupRepositorySecretsPort: this.setupRepositorySecretsPort, }); } } @@ -51631,6 +54398,7 @@ const result_1 = __nccwpck_require__(73817); const version_policy_1 = __nccwpck_require__(8381); const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); +const setup_configuration_policy_1 = __nccwpck_require__(56637); const TASK_ID = 'InitialSetupUseCase'; /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ async function runInitialSetupWorkflow(param, dependencies) { @@ -51638,14 +54406,23 @@ async function runInitialSetupWorkflow(param, dependencies) { const steps = []; const errors = []; try { - (0, logging_ports_1.logInfo)('📋 Ensuring .github and copying setup files...'); - const filesResult = dependencies.setupWorkspacePort.prepare(); - steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); - if (!dependencies.setupWorkspacePort.hasValidToken()) { - (0, logging_ports_1.logInfo)(' 🛑 Setup requires PERSONAL_ACCESS_TOKEN (environment or .env) with a valid token.'); - errors.push('PERSONAL_ACCESS_TOKEN must be set (environment or .env) with a valid token to run setup.'); + const setupConfiguration = getSetupConfiguration(param); + if (!dependencies.setupWorkspacePort.hasValidToken(param.tokens.token)) { + (0, logging_ports_1.logInfo)(' 🛑 Setup requires the setup PAT provided for this command with a valid token.'); + errors.push('A valid setup PAT must be provided to run setup. It is separate from the workflow PAT Secret.'); return [buildResult(errors, steps)]; } + (0, logging_ports_1.logInfo)('📋 Ensuring .github and copying setup files...'); + const workflowUpdates = getWorkflowUpdates(param); + const workspaceSelection = { + features: setupConfiguration?.features, + ...(workflowUpdates.length > 0 ? { + updateExistingWorkflows: true, + approvedWorkflowFiles: workflowUpdates, + } : {}), + }; + const filesResult = dependencies.setupWorkspacePort.prepare(workspaceSelection); + steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); (0, logging_ports_1.logInfo)('🔐 Checking GitHub access...'); const githubAccess = await verifyGitHubAccess(param, dependencies.authenticatedUserPort); if (!githubAccess.success) { @@ -51653,6 +54430,11 @@ async function runInitialSetupWorkflow(param, dependencies) { return [buildResult(errors, steps)]; } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); + const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration); + if (secrets.step) + steps.push(secrets.step); + if (secrets.errors.length > 0) + errors.push(...secrets.errors); (0, logging_ports_1.logInfo)('🏷️ Checking configured and progress labels...'); const labels = await ensureInitialLabels(param, dependencies.initialLabelProvisioningPort); if (!labels.completed) { @@ -51670,7 +54452,12 @@ async function runInitialSetupWorkflow(param, dependencies) { else { steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const defaultVersion = await ensureDefaultVersion(param, dependencies); + const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration); + if (variables.step) + steps.push(variables.step); + if (variables.errors.length > 0) + errors.push(...variables.errors); + const defaultVersion = await ensureDefaultVersion(param, dependencies, setupConfiguration); if (defaultVersion.step) steps.push(defaultVersion.step); if (defaultVersion.error) @@ -51719,7 +54506,10 @@ async function ensureIssueTypes(param, repository) { return { success: false, created: 0, existing: 0, errors: [`Error ensuring issue types: ${error}`] }; } } -async function ensureDefaultVersion(param, dependencies) { +async function ensureDefaultVersion(param, dependencies, setupConfiguration) { + if (setupConfiguration?.createInitialTag === false) { + return { step: '⏭️ Initial version tag creation disabled by setup configuration.' }; + } try { const existingTag = await dependencies.latestTagQueryPort.getLatestTag(); if (existingTag !== undefined) { @@ -51744,6 +54534,70 @@ async function ensureDefaultVersion(param, dependencies) { return { error: message }; } } +function getSetupConfiguration(param) { + const configuration = param.inputs?.setupConfiguration; + return configuration && typeof configuration === 'object' + ? configuration + : undefined; +} +function getWorkflowUpdates(param) { + const updates = param.inputs?.setupWorkflowUpdates; + return Array.isArray(updates) ? updates.filter((file) => typeof file === 'string') : []; +} +async function ensureRepositoryVariables(param, dependencies, setupConfiguration) { + if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { + return { errors: [] }; + } + try { + const result = await dependencies.setupRepositoryVariablesPort.upsert(param.owner, param.repo, param.tokens.token, (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration)); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ Repository Variables: ${result.created} created, ${result.updated} updated`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Variables: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +async function ensureRepositorySecrets(param, dependencies, setupConfiguration) { + if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { + return { errors: [] }; + } + const credentials = getSetupCredentialCollection(param); + if (!credentials) { + return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; + } + const values = [ + ...(credentials.workflowPat ? [credentials.workflowPat] : []), + ...credentials.apiKeys, + ]; + if (values.length === 0) + return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; + try { + const result = await dependencies.setupRepositorySecretsPort.upsertSecrets(param.owner, param.repo, param.tokens.token, values); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ Repository Secrets: ${result.created} created, ${result.updated} updated; existing values kept when no replacement was selected.`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Secrets: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +function getSetupCredentialCollection(param) { + const credentials = param.inputs?.setupCredentials; + if (!credentials || typeof credentials !== 'object') + return undefined; + return credentials; +} function appendLabelSummary(steps, errors, summary, labelType) { if (summary.errors.length > 0) { errors.push(...summary.errors); @@ -63488,7 +66342,7 @@ async function ensureConfiguredIssueTypeSafely(client, owner, configured) { catch (error) { const message = error instanceof Error ? error.message : String(error); (0, logger_1.logError)(`Error ensuring issue type "${configured.name}": ${error}`); - return { kind: 'error', message: `Error creando tipo de Issue "${configured.name}": ${message}` }; + return { kind: 'error', message: `Error creating Issue type "${configured.name}": ${message}` }; } } function ensureConfiguredIssueType(client, owner, configured) { @@ -63533,22 +66387,22 @@ async function listIssueTypes(client, owner) { const response = await client.graphql(ISSUE_TYPES_QUERY, { owner, after: cursor }); const organization = response.organization; if (!organization) - throw new Error(`No se pudo obtener la organización ${owner}`); + throw new Error(`Could not resolve the organization ${owner}`); issueTypes.push(...organization.issueTypes.nodes); const pageInfo = organization.issueTypes.pageInfo; if (!pageInfo?.hasNextPage) return issueTypes; if (!pageInfo.endCursor) { - throw new Error(`La paginación de tipos de Issue no devolvió cursor en la página ${page}.`); + throw new Error(`Issue type pagination did not return a cursor on page ${page}.`); } cursor = pageInfo.endCursor; } - throw new Error("La paginación de tipos de Issue superó 100 páginas."); + throw new Error('Issue type pagination exceeded 100 pages.'); } async function createIssueType(client, owner, name, description, color) { const response = await client.graphql(ORGANIZATION_ID_QUERY, { owner }); if (!response.organization) - throw new Error(`No se pudo obtener la organización ${owner}`); + throw new Error(`Could not resolve the organization ${owner}`); const result = await client.graphql(CREATE_ISSUE_TYPE_MUTATION, { ownerId: response.organization.id, name, @@ -65661,6 +68515,114 @@ function releaseIdAsString(id) { } +/***/ }), + +/***/ 28493: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.RepositoryVariablesRepository = void 0; +exports.encryptSecret = encryptSecret; +const tweetnacl_1 = __importDefault(__nccwpck_require__(24258)); +const node_crypto_1 = __nccwpck_require__(6005); +class RepositoryVariablesRepository { + constructor(githubClient) { + this.githubClient = githubClient; + } + async list(owner, repository, token) { + const client = this.githubClient.getClient(token); + if (!client.rest.secrets) + throw new Error('GitHub repository Secret API is unavailable.'); + const response = await client.rest.secrets.listRepoSecrets({ owner, repo: repository, per_page: 100 }); + return response.data.secrets.map(secret => secret.name); + } + async listVariables(owner, repository, token) { + const client = this.githubClient.getClient(token); + const response = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); + return response.data.variables.map(variable => ({ name: variable.name, value: variable.value })); + } + async upsertSecrets(owner, repository, token, credentials) { + const client = this.githubClient.getClient(token); + if (!client.rest.secrets) + throw new Error('GitHub repository Secret API is unavailable.'); + const existing = new Set(await this.list(owner, repository, token)); + const publicKey = await client.rest.secrets.getRepoPublicKey({ owner, repo: repository }); + let created = 0; + let updated = 0; + const skipped = 0; + const errors = []; + for (const credential of credentials) { + try { + await client.rest.secrets.createOrUpdateRepoSecret({ + owner, + repo: repository, + secret_name: credential.name, + encrypted_value: encryptSecret(credential.value, publicKey.data.key), + key_id: publicKey.data.key_id, + }); + if (existing.has(credential.name)) + updated += 1; + else + created += 1; + } + catch (error) { + errors.push(`Error configuring repository Secret ${credential.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, skipped, errors }; + } + /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ + async upsert(owner, repository, token, variables) { + return this.upsertVariables(owner, repository, token, variables); + } + async upsertVariables(owner, repository, token, variables) { + const client = this.githubClient.getClient(token); + const existing = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); + const existingValues = new Map(existing.data.variables.map(variable => [variable.name, variable.value])); + let created = 0; + let updated = 0; + const errors = []; + for (const variable of variables) { + try { + if (existingValues.has(variable.name)) { + if (existingValues.get(variable.name) === variable.value) + continue; + await client.rest.actions.updateRepoVariable({ owner, repo: repository, name: variable.name, value: variable.value }); + updated += 1; + } + else { + await client.rest.actions.createRepoVariable({ owner, repo: repository, name: variable.name, value: variable.value }); + created += 1; + } + } + catch (error) { + errors.push(`Error configuring repository Variable ${variable.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, errors }; + } +} +exports.RepositoryVariablesRepository = RepositoryVariablesRepository; +/** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ +function encryptSecret(value, base64PublicKey) { + const publicKey = Buffer.from(base64PublicKey, 'base64'); + if (publicKey.length !== tweetnacl_1.default.box.publicKeyLength) + throw new Error('GitHub returned an invalid repository public key.'); + const keyPair = tweetnacl_1.default.box.keyPair(); + const nonce = (0, node_crypto_1.createHash)('blake2b512') + .update(Buffer.concat([Buffer.from(keyPair.publicKey), publicKey])) + .digest() + .subarray(0, tweetnacl_1.default.box.nonceLength); + const ciphertext = tweetnacl_1.default.box(Buffer.from(value, 'utf8'), nonce, publicKey, keyPair.secretKey); + return Buffer.from(Buffer.concat([Buffer.from(keyPair.publicKey), Buffer.from(ciphertext)])).toString('base64'); +} + + /***/ }), /***/ 40941: @@ -66675,14 +69637,17 @@ function createGithubExecutionAdmissionUseCase() { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createOrganizationMembersClient = exports.createActorAuthorizationClient = exports.createAuthenticatedUserClient = void 0; +exports.createRepositoryVariablesClient = exports.createOrganizationMembersClient = exports.createActorAuthorizationClient = exports.createAuthenticatedUserClient = void 0; const octokit_identity_adapters_1 = __nccwpck_require__(29996); +const octokit_repository_variables_adapter_1 = __nccwpck_require__(81329); const createAuthenticatedUserClient = () => new octokit_identity_adapters_1.OctokitAuthenticatedUserClientAdapter(); exports.createAuthenticatedUserClient = createAuthenticatedUserClient; const createActorAuthorizationClient = () => new octokit_identity_adapters_1.OctokitActorAuthorizationClientAdapter(); exports.createActorAuthorizationClient = createActorAuthorizationClient; const createOrganizationMembersClient = () => new octokit_identity_adapters_1.OctokitOrganizationMembersClientAdapter(); exports.createOrganizationMembersClient = createOrganizationMembersClient; +const createRepositoryVariablesClient = () => new octokit_repository_variables_adapter_1.OctokitRepositoryVariablesClientAdapter(); +exports.createRepositoryVariablesClient = createRepositoryVariablesClient; /***/ }), @@ -66799,9 +69764,12 @@ const repository_tag_repository_1 = __nccwpck_require__(58717); const git_cli_repository_1 = __nccwpck_require__(26331); const initial_setup_use_case_composition_1 = __nccwpck_require__(93141); const setup_workspace_adapter_1 = __nccwpck_require__(5729); +const repository_variables_repository_1 = __nccwpck_require__(28493); +const github_identity_client_factory_2 = __nccwpck_require__(93081); function createInitialSetupCompositionRoot() { const labelProvisioning = new issue_label_provisioning_repository_1.IssueLabelProvisioningRepository((0, github_issue_client_factory_1.createIssueLabelProvisioningClient)()); - return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter()); + const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_2.createRepositoryVariablesClient)()); + return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter(), repositoryConfiguration, repositoryConfiguration); } @@ -67597,6 +70565,24 @@ class OctokitReleaseClientAdapter { exports.OctokitReleaseClientAdapter = OctokitReleaseClientAdapter; +/***/ }), + +/***/ 81329: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OctokitRepositoryVariablesClientAdapter = void 0; +const octokit_client_resolver_1 = __nccwpck_require__(54047); +class OctokitRepositoryVariablesClientAdapter { + getClient(token) { + return (0, octokit_client_resolver_1.getOctokitClient)(token); + } +} +exports.OctokitRepositoryVariablesClientAdapter = OctokitRepositoryVariablesClientAdapter; + + /***/ }), /***/ 86719: @@ -67709,13 +70695,23 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupWorkspaceAdapter = void 0; const setup_files_1 = __nccwpck_require__(59126); class SetupWorkspaceAdapter { - prepare() { + prepare(selection) { const workspace = process.cwd(); (0, setup_files_1.ensureGitHubDirs)(workspace); - return (0, setup_files_1.copySetupFiles)(workspace); + if (!selection) + return (0, setup_files_1.copySetupFiles)(workspace); + return (0, setup_files_1.copySetupFiles)(workspace, undefined, selection?.features, { + updateExistingWorkflows: selection?.updateExistingWorkflows, + approvedWorkflowFiles: selection?.approvedWorkflowFiles, + }); + } + hasValidToken(tokenOverride) { + return tokenOverride === undefined + ? (0, setup_files_1.hasValidSetupToken)(process.cwd()) + : (0, setup_files_1.hasValidSetupToken)(process.cwd(), tokenOverride); } - hasValidToken() { - return (0, setup_files_1.hasValidSetupToken)(process.cwd()); + compareWorkflows(features) { + return (0, setup_files_1.compareSetupWorkflows)(process.cwd(), features); } } exports.SetupWorkspaceAdapter = SetupWorkspaceAdapter; @@ -69422,24 +72418,28 @@ exports.copySetupDirectory = copySetupDirectory; const fs = __importStar(__nccwpck_require__(57147)); const path = __importStar(__nccwpck_require__(71017)); const logger_1 = __nccwpck_require__(91151); -function copySetupFile(source, destination, displaySource, displayDestination) { +function copySetupFile(source, destination, displaySource, displayDestination, options = {}) { if (!fs.existsSync(source)) return { copied: 0, skipped: 0 }; - if (fs.existsSync(destination)) { + if (fs.existsSync(destination) && !options.overwrite) { (0, logger_1.logInfo)(` ⏭️ ${displayDestination} already exists; skipping.`); return { copied: 0, skipped: 1 }; } + if (fs.existsSync(destination) && options.backupDirectory) { + fs.mkdirSync(options.backupDirectory, { recursive: true }); + fs.copyFileSync(destination, path.join(options.backupDirectory, path.basename(destination))); + } fs.copyFileSync(source, destination); - (0, logger_1.logInfo)(` ✅ Copied ${displaySource} → ${displayDestination}`); + (0, logger_1.logInfo)(` ${options.overwrite ? '↻ Updated' : '✅ Copied'} ${displaySource} → ${displayDestination}`); return { copied: 1, skipped: 0 }; } -function copySetupDirectory(sourceDirectory, destinationDirectory, fileFilter, displayDirectory) { +function copySetupDirectory(sourceDirectory, destinationDirectory, fileFilter, displayDirectory, options = {}) { if (!fs.existsSync(sourceDirectory)) return { copied: 0, skipped: 0 }; return fs.readdirSync(sourceDirectory) .filter(fileFilter) .filter((fileName) => fs.statSync(path.join(sourceDirectory, fileName)).isFile()) - .map((fileName) => copySetupFile(path.join(sourceDirectory, fileName), path.join(destinationDirectory, fileName), `${displayDirectory}/${fileName}`, `${displayDirectory.replace('setup/', '.github/')}/${fileName}`)) + .map((fileName) => copySetupFile(path.join(sourceDirectory, fileName), path.join(destinationDirectory, fileName), `${displayDirectory}/${fileName}`, `${displayDirectory.replace('setup/', '.github/')}/${fileName}`, options)) .reduce((total, current) => ({ copied: total.copied + current.copied, skipped: total.skipped + current.skipped, @@ -69490,10 +72490,9 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ensureGitHubDirs = ensureGitHubDirs; exports.copySetupFiles = copySetupFiles; -exports.ensureEnvWithToken = ensureEnvWithToken; +exports.compareSetupWorkflows = compareSetupWorkflows; exports.getSetupToken = getSetupToken; exports.hasValidSetupToken = hasValidSetupToken; -exports.setupEnvFileExists = setupEnvFileExists; const fs = __importStar(__nccwpck_require__(57147)); const path = __importStar(__nccwpck_require__(71017)); const setup_file_copy_1 = __nccwpck_require__(90102); @@ -69528,57 +72527,74 @@ function ensureGitHubDirs(cwd) { * @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root. * @returns { copied, skipped } */ -function copySetupFiles(cwd, setupDirOverride) { +function copySetupFiles(cwd, setupDirOverride, features, options = {}) { const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup'); if (!fs.existsSync(setupDir)) return { copied: 0, skipped: 0 }; - const workflows = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'workflows'), path.join(cwd, '.github', 'workflows'), (fileName) => fileName.endsWith('.yml') || fileName.endsWith('.yaml'), 'setup/workflows'); - const issueTemplates = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'ISSUE_TEMPLATE'), path.join(cwd, '.github', 'ISSUE_TEMPLATE'), () => true, 'setup/ISSUE_TEMPLATE'); - const pullRequestTemplate = (0, setup_file_copy_1.copySetupFile)(path.join(setupDir, 'pull_request_template.md'), path.join(cwd, '.github', 'pull_request_template.md'), 'setup/pull_request_template.md', '.github/pull_request_template.md'); - // Credentials are deliberately never copied from the package. Keep the - // destination check here so setup can guide users to their local .env. - ensureEnvWithToken(cwd); + const workflowFeatures = { + 'copilot_issue.yml': 'issues', + 'copilot_pull_request.yml': 'pullRequests', + 'copilot_commit.yml': 'commits', + 'copilot_issue_comment.yml': 'issueComments', + 'copilot_pull_request_comment.yml': 'pullRequestComments', + 'release_workflow.yml': 'release', + 'hotfix_workflow.yml': 'hotfix', + 'agent-cli-provisioning.yml': 'agentProvisioning', + 'copilot_credential_health.yml': 'credentialHealth', + }; + const approvedWorkflowFiles = new Set(options.approvedWorkflowFiles ?? []); + const backupDirectory = options.updateExistingWorkflows ? path.join(cwd, '.copilot', 'setup-backups', new Date().toISOString().replace(/[:.]/g, '-')) : undefined; + const workflows = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'workflows'), path.join(cwd, '.github', 'workflows'), (fileName) => (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) + && (features === undefined || features[workflowFeatures[fileName]] !== false) + && (!options.updateExistingWorkflows + || approvedWorkflowFiles.has(fileName) + || !fs.existsSync(path.join(cwd, '.github', 'workflows', fileName))), 'setup/workflows', { + overwrite: options.updateExistingWorkflows, + backupDirectory, + }); + const issueTemplates = (0, setup_file_copy_1.copySetupDirectory)(path.join(setupDir, 'ISSUE_TEMPLATE'), path.join(cwd, '.github', 'ISSUE_TEMPLATE'), (fileName) => features?.issueTemplates !== false + && (features?.release !== false || fileName !== 'release.yml') + && (features?.hotfix !== false || fileName !== 'hotfix.yml'), 'setup/ISSUE_TEMPLATE'); + const pullRequestTemplate = features?.pullRequestTemplate === false + ? { copied: 0, skipped: 0 } + : (0, setup_file_copy_1.copySetupFile)(path.join(setupDir, 'pull_request_template.md'), path.join(cwd, '.github', 'pull_request_template.md'), 'setup/pull_request_template.md', '.github/pull_request_template.md'); return [workflows, issueTemplates, pullRequestTemplate].reduce((total, current) => ({ copied: total.copied + current.copied, skipped: total.skipped + current.skipped, }), { copied: 0, skipped: 0 }); } +function compareSetupWorkflows(cwd, features, setupDirOverride) { + const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup'); + const workflowFeatures = { + 'copilot_issue.yml': 'issues', + 'copilot_pull_request.yml': 'pullRequests', + 'copilot_commit.yml': 'commits', + 'copilot_issue_comment.yml': 'issueComments', + 'copilot_pull_request_comment.yml': 'pullRequestComments', + 'release_workflow.yml': 'release', + 'hotfix_workflow.yml': 'hotfix', + 'agent-cli-provisioning.yml': 'agentProvisioning', + 'copilot_credential_health.yml': 'credentialHealth', + }; + const sourceDirectory = path.join(setupDir, 'workflows'); + if (!fs.existsSync(sourceDirectory)) + return []; + return fs.readdirSync(sourceDirectory) + .filter(file => (file.endsWith('.yml') || file.endsWith('.yaml')) && (features === undefined || features[workflowFeatures[file]] !== false)) + .filter(file => fs.statSync(path.join(sourceDirectory, file)).isFile()) + .map(file => { + const source = path.join(sourceDirectory, file); + const destination = path.join(cwd, '.github', 'workflows', file); + if (!fs.existsSync(destination)) + return { file, destination: `.github/workflows/${file}`, status: 'missing' }; + const equal = fs.readFileSync(source, 'utf8') === fs.readFileSync(destination, 'utf8'); + return { file, destination: `.github/workflows/${file}`, status: equal ? 'unchanged' : 'changed' }; + }); +} const ENV_TOKEN_KEY = 'PERSONAL_ACCESS_TOKEN'; const ENV_PLACEHOLDER_VALUE = 'github_pat_11..'; /** Minimum length for a token to be considered "defined" (not placeholder). */ const MIN_VALID_TOKEN_LENGTH = 20; -function getTokenFromEnvFile(envPath) { - if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) - return null; - const content = fs.readFileSync(envPath, 'utf8'); - const match = content.match(new RegExp(`^${ENV_TOKEN_KEY}=(.+)$`, 'm')); - if (!match) - return null; - const value = match[1].trim().replace(/^["']|["']$/g, ''); - return value.length > 0 ? value : null; -} -/** - * Logs the current state of PERSONAL_ACCESS_TOKEN (environment or .env). Does not create .env. - */ -function ensureEnvWithToken(cwd) { - const envPath = path.join(cwd, '.env'); - const tokenInEnv = process.env[ENV_TOKEN_KEY]?.trim(); - if (tokenInEnv) { - (0, logger_1.logInfo)(' 🔑 PERSONAL_ACCESS_TOKEN is set in environment; .env not needed.'); - return; - } - if (fs.existsSync(envPath)) { - const tokenInFile = getTokenFromEnvFile(envPath); - if (tokenInFile) { - (0, logger_1.logInfo)(' ✅ .env exists and contains PERSONAL_ACCESS_TOKEN.'); - } - else { - (0, logger_1.logInfo)(' ⚠️ .env exists but PERSONAL_ACCESS_TOKEN is missing or empty.'); - } - return; - } - (0, logger_1.logInfo)(' 💡 You can create a .env file here with PERSONAL_ACCESS_TOKEN=your_token or set it in your environment.'); -} function isTokenValueValid(token) { const t = token.trim(); return t.length >= MIN_VALID_TOKEN_LENGTH && t !== ENV_PLACEHOLDER_VALUE; @@ -69586,21 +72602,16 @@ function isTokenValueValid(token) { /** * Resolves the PERSONAL_ACCESS_TOKEN for setup from a single priority order: * 1. override (e.g. CLI --token) if provided and valid, - * 2. process.env.PERSONAL_ACCESS_TOKEN, - * 3. .env file in cwd. + * 2. process.env.PERSONAL_ACCESS_TOKEN. * Returns undefined if no valid token is found. */ -function getSetupToken(cwd, override) { +function getSetupToken(_cwd, override) { const overrideTrimmed = override?.trim(); if (overrideTrimmed && isTokenValueValid(overrideTrimmed)) return overrideTrimmed; const fromEnv = process.env[ENV_TOKEN_KEY]?.trim(); if (fromEnv && isTokenValueValid(fromEnv)) return fromEnv; - const envPath = path.join(cwd, '.env'); - const fromFile = getTokenFromEnvFile(envPath); - if (fromFile !== null && isTokenValueValid(fromFile)) - return fromFile; return undefined; } /** @@ -69610,11 +72621,6 @@ function getSetupToken(cwd, override) { function hasValidSetupToken(cwd, override) { return getSetupToken(cwd, override) !== undefined; } -/** Returns true if a .env file exists in the given directory. */ -function setupEnvFileExists(cwd) { - const envPath = path.join(cwd, '.env'); - return fs.existsSync(envPath) && fs.statSync(envPath).isFile(); -} /***/ }), diff --git a/build/github_action/src/application/policies/setup_configuration_policy.d.ts b/build/github_action/src/application/policies/setup_configuration_policy.d.ts new file mode 100644 index 00000000..ade1bcf4 --- /dev/null +++ b/build/github_action/src/application/policies/setup_configuration_policy.d.ts @@ -0,0 +1,23 @@ +import type { AgentTask } from '../../domain/agent'; +import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement } from '../../domain/setup'; +export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; +export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; +export declare function createDefaultSetupConfiguration(): SetupConfiguration; +export type SetupConfigurationOverrides = { + features?: Partial; + agents?: Partial>>; + repository?: Partial; + ai?: Partial; + projects?: Partial; + createInitialTag?: boolean; + manageRepositoryVariables?: boolean; + manageRepositorySecrets?: boolean; + actionInputs?: Record; +}; +export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; +export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; +export declare function buildSetupPlan(configuration: SetupConfiguration): SetupPlan; +/** Builds the non-sensitive credential contract implied by the selected agents. */ +export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; +export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; +export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; diff --git a/build/github_action/src/application/ports/setup_wizard_ports.d.ts b/build/github_action/src/application/ports/setup_wizard_ports.d.ts new file mode 100644 index 00000000..3ede0673 --- /dev/null +++ b/build/github_action/src/application/ports/setup_wizard_ports.d.ts @@ -0,0 +1,53 @@ +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, DoctorCheck } from '../../domain/setup'; +export interface SetupPromptPort { + collect(defaults: SetupConfiguration): Promise; + showPlan(plan: SetupPlan): void; + confirm(plan: SetupPlan): Promise; + close(): void; +} +export interface SetupCredentialPromptPort { + requestSetupPat(): Promise; + explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; + requestWorkflowPat(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + requestApiKey(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + chooseExistingCredential(requirement: SetupCredentialRequirement, check: SetupCredentialCheck): Promise; + showCredentialChecks(checks: readonly SetupCredentialCheck[]): void; +} +export interface SetupRepositorySecretsPort { + list(owner: string, repository: string, token: string): Promise; + upsertSecrets(owner: string, repository: string, token: string, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; +} +export interface SetupRepositoryConfigurationReadPort { + listVariables(owner: string, repository: string, token: string): Promise; +} +export interface DoctorOutputPort { + showDoctorChecks(checks: readonly DoctorCheck[]): void; +} +export interface SetupCredentialValidationPort { + validateSetupPat(owner: string, repository: string, token: string): Promise; + validateCredential(requirement: SetupCredentialRequirement, value: string): Promise; +} +export interface SetupRemoteCredentialHealthPort { + validateExisting(owner: string, repository: string, token: string, ref: string, requirements: readonly SetupCredentialRequirement[]): Promise; +} +export interface SetupWorkflowUpdatePromptPort { + confirmWorkflowUpdates(comparisons: readonly SetupWorkflowComparison[], forcedByFlag: boolean): Promise; +} +export interface SetupRepositoryVariablesPort { + upsert(owner: string, repository: string, token: string, variables: readonly { + name: string; + value: string; + }[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; +} diff --git a/build/github_action/src/application/ports/setup_workspace_ports.d.ts b/build/github_action/src/application/ports/setup_workspace_ports.d.ts index 822fa1ff..6588c092 100644 --- a/build/github_action/src/application/ports/setup_workspace_ports.d.ts +++ b/build/github_action/src/application/ports/setup_workspace_ports.d.ts @@ -1,8 +1,15 @@ +import type { SetupFeatures, SetupWorkflowComparison } from '../../domain/setup'; export interface SetupWorkspaceResult { copied: number; skipped: number; } +export interface SetupWorkspaceSelection { + features?: SetupFeatures; + updateExistingWorkflows?: boolean; + approvedWorkflowFiles?: readonly string[]; +} export interface SetupWorkspacePort { - prepare(): SetupWorkspaceResult; - hasValidToken(): boolean; + prepare(selection?: SetupWorkspaceSelection): SetupWorkspaceResult; + hasValidToken(tokenOverride?: string): boolean; + compareWorkflows?(features?: SetupFeatures): readonly SetupWorkflowComparison[]; } diff --git a/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts b/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts index 2c5cab5a..aab353e2 100644 --- a/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts +++ b/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts @@ -6,6 +6,7 @@ import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { ParamUseCase } from '../base/param_usecase'; +import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; /** Application boundary for provisioning a repository for Copilot automation. */ export declare class InitialSetupUseCase implements ParamUseCase { private readonly authenticatedUserPort; @@ -15,7 +16,9 @@ export declare class InitialSetupUseCase implements ParamUseCase; } diff --git a/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts b/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts index 64ff3bd0..5bc88b49 100644 --- a/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts +++ b/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts @@ -5,6 +5,7 @@ import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports/repository_release_ports'; import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; +import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; export interface InitialSetupWorkflowDependencies { authenticatedUserPort: AuthenticatedUserPort; initialLabelProvisioningPort: InitialLabelProvisioningPort; @@ -13,6 +14,8 @@ export interface InitialSetupWorkflowDependencies { repositoryDefaultBranchPort: RepositoryDefaultBranchPort; repositoryTagPort: RepositoryTagPort; setupWorkspacePort: SetupWorkspacePort; + setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; + setupRepositorySecretsPort?: SetupRepositorySecretsPort; } /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ export declare function runInitialSetupWorkflow(param: Execution, dependencies: InitialSetupWorkflowDependencies): Promise; diff --git a/build/github_action/src/application/usecases/setup/doctor_use_case.d.ts b/build/github_action/src/application/usecases/setup/doctor_use_case.d.ts new file mode 100644 index 00000000..16692f6f --- /dev/null +++ b/build/github_action/src/application/usecases/setup/doctor_use_case.d.ts @@ -0,0 +1,19 @@ +import type { SetupConfiguration } from '../../../domain/setup'; +import type { DoctorOutputPort, SetupCredentialValidationPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; +export interface DoctorRequest { + owner: string; + repository: string; + setupToken: string; + configuration: SetupConfiguration; +} +export declare class SetupDoctorUseCase { + private readonly validation; + private readonly secrets; + private readonly variables; + private readonly workspace; + private readonly output; + private readonly remoteHealth?; + constructor(validation: SetupCredentialValidationPort, secrets: SetupRepositorySecretsPort, variables: SetupRepositoryConfigurationReadPort, workspace: SetupWorkspacePort, output: DoctorOutputPort, remoteHealth?: SetupRemoteCredentialHealthPort | undefined); + execute(request: DoctorRequest): Promise; +} diff --git a/build/github_action/src/application/usecases/setup/index.d.ts b/build/github_action/src/application/usecases/setup/index.d.ts new file mode 100644 index 00000000..81102e29 --- /dev/null +++ b/build/github_action/src/application/usecases/setup/index.d.ts @@ -0,0 +1,4 @@ +export { SetupWizardUseCase } from './setup_wizard_use_case'; +export type { SetupWizardRequest } from './setup_wizard_use_case'; +export { SetupCredentialsUseCase } from './setup_credentials_use_case'; +export type { SetupCredentialsRequest, SetupCredentialsResult } from './setup_credentials_use_case'; diff --git a/build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts b/build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts new file mode 100644 index 00000000..a6a3295f --- /dev/null +++ b/build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts @@ -0,0 +1,24 @@ +import type { SetupCredentialCheck, SetupCredentialCollection, SetupCredentialRequirement } from '../../../domain/setup'; +import type { SetupCredentialPromptPort, SetupCredentialValidationPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +export interface SetupCredentialsRequest { + owner: string; + repository: string; + setupToken: string; + requirements: readonly SetupCredentialRequirement[]; + manageSecrets: boolean; + ref?: string; +} +export interface SetupCredentialsResult { + collection: SetupCredentialCollection; + checks: SetupCredentialCheck[]; + existingSecretNames: readonly string[]; +} +/** Coordinates secret collection and validation without placing secret values in config files. */ +export declare class SetupCredentialsUseCase { + private readonly prompt; + private readonly validation; + private readonly secrets?; + private readonly remoteHealth?; + constructor(prompt: SetupCredentialPromptPort, validation: SetupCredentialValidationPort, secrets?: SetupRepositorySecretsPort | undefined, remoteHealth?: SetupRemoteCredentialHealthPort | undefined); + collect(request: SetupCredentialsRequest): Promise; +} diff --git a/build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts b/build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts new file mode 100644 index 00000000..ad5dfab9 --- /dev/null +++ b/build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts @@ -0,0 +1,14 @@ +import type { SetupPromptPort } from '../../ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan } from '../../../domain/setup'; +import { type SetupConfigurationOverrides } from '../../policies/setup_configuration_policy'; +export interface SetupWizardRequest { + overrides?: SetupConfigurationOverrides; + skipRepositoryVariables?: boolean; +} +export declare class SetupWizardUseCase { + private readonly prompt; + constructor(prompt: SetupPromptPort); + collect(request?: SetupWizardRequest): Promise; + plan(configuration: SetupConfiguration): SetupPlan; + close(): void; +} diff --git a/build/github_action/src/cli/commands/doctor.d.ts b/build/github_action/src/cli/commands/doctor.d.ts new file mode 100644 index 00000000..a10e18a5 --- /dev/null +++ b/build/github_action/src/cli/commands/doctor.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function registerDoctorCommand(program: Command): void; diff --git a/build/github_action/src/cli/commands/setup_policy.d.ts b/build/github_action/src/cli/commands/setup_policy.d.ts index 8e359df8..670911a0 100644 --- a/build/github_action/src/cli/commands/setup_policy.d.ts +++ b/build/github_action/src/cli/commands/setup_policy.d.ts @@ -1,5 +1,6 @@ import type { GitInfo } from '../../cli_context'; +import type { SetupConfiguration, SetupCredentialCollection } from '../../domain/setup'; export interface SetupCommandOptions { debug?: boolean; } -export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string): Record | undefined; +export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string, configuration?: SetupConfiguration, credentials?: SetupCredentialCollection, approvedWorkflowFiles?: readonly string[]): Record | undefined; diff --git a/build/github_action/src/cli/setup_config_file.d.ts b/build/github_action/src/cli/setup_config_file.d.ts new file mode 100644 index 00000000..667ccae4 --- /dev/null +++ b/build/github_action/src/cli/setup_config_file.d.ts @@ -0,0 +1,3 @@ +import { type SetupConfigurationOverrides } from '../application/policies/setup_configuration_policy'; +/** Loads a non-secret setup override file. JSON and YAML are supported. */ +export declare function loadSetupConfigurationOverrides(filePath: string): SetupConfigurationOverrides; diff --git a/build/github_action/src/cli/setup_prompt_adapter.d.ts b/build/github_action/src/cli/setup_prompt_adapter.d.ts new file mode 100644 index 00000000..1ad2dd3d --- /dev/null +++ b/build/github_action/src/cli/setup_prompt_adapter.d.ts @@ -0,0 +1,32 @@ +import type { SetupCredentialPromptPort, SetupPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort } from '../application/ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison } from '../domain/setup'; +export interface SetupPromptAdapterOptions { + interactive?: boolean; + assumeYes?: boolean; + credentialValues?: Record; +} +export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { + private readonly interactive; + private readonly assumeYes; + private readonly readline; + private readonly credentialValues; + constructor(options?: SetupPromptAdapterOptions); + collect(defaults: SetupConfiguration): Promise; + showPlan(plan: SetupPlan): void; + confirm(plan: SetupPlan): Promise; + requestSetupPat(): Promise; + explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; + requestWorkflowPat(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + requestApiKey(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + chooseExistingCredential(requirement: SetupCredentialRequirement, check: SetupCredentialCheck): Promise; + showCredentialChecks(checks: readonly SetupCredentialCheck[]): void; + showDoctorChecks(checks: readonly import('../domain/setup').DoctorCheck[]): void; + confirmWorkflowUpdates(comparisons: readonly SetupWorkflowComparison[], forcedByFlag: boolean): Promise; + close(): void; + private askText; + private requestSecretForRequirement; + private askSecret; + private askNumber; + private askBoolean; + private askChoice; +} diff --git a/build/github_action/src/data/repository/repository_variables_repository.d.ts b/build/github_action/src/data/repository/repository_variables_repository.d.ts new file mode 100644 index 00000000..bd7ceeb5 --- /dev/null +++ b/build/github_action/src/data/repository/repository_variables_repository.d.ts @@ -0,0 +1,31 @@ +import type { SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialValue } from '../../domain/setup'; +import type { GithubClientPort } from '../../infrastructure/github/ports/github_client_provider_port'; +import type { GithubRepositoryVariablesClient } from '../../infrastructure/github/ports/github_repository_variables_protocol'; +export declare class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort { + private readonly githubClient; + constructor(githubClient: GithubClientPort); + list(owner: string, repository: string, token: string): Promise; + listVariables(owner: string, repository: string, token: string): Promise; + upsertSecrets(owner: string, repository: string, token: string, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; + /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ + upsert(owner: string, repository: string, token: string, variables: readonly { + name: string; + value: string; + }[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; + private upsertVariables; +} +/** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ +export declare function encryptSecret(value: string, base64PublicKey: string): string; diff --git a/build/github_action/src/domain/setup.d.ts b/build/github_action/src/domain/setup.d.ts new file mode 100644 index 00000000..e02a2dcf --- /dev/null +++ b/build/github_action/src/domain/setup.d.ts @@ -0,0 +1,110 @@ +import type { AgentProvider, AgentTask } from './agent'; +export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'issueTemplates' | 'pullRequestTemplate'; +export interface SetupFeatures { + [feature: string]: boolean; +} +export interface SetupAgentRoleConfiguration { + provider: AgentProvider; + modelProvider: string; + model: string; + effort?: string; +} +export type SetupAgentConfiguration = Record; +export interface SetupRepositoryConfiguration { + mainBranch: string; + developmentBranch: string; + featureTree: string; + bugfixTree: string; + hotfixTree: string; + releaseTree: string; + docsTree: string; + choreTree: string; + branchManagementAlways: boolean; + reopenIssueOnPush: boolean; + desiredAssigneesCount: number; + desiredReviewersCount: number; + mergeTimeout: number; + issueLocale: string; + pullRequestLocale: string; + commitPrefixTransforms: string; +} +export interface SetupAiConfiguration { + pullRequestDescription: boolean; + ignoreFiles: string; + membersOnly: boolean; + includeReasoning: boolean; + bugbotSeverity: 'info' | 'low' | 'medium' | 'high'; + bugbotCommentLimit: number; + bugbotFixVerifyCommands: string; + provisioningMode: 'auto' | 'always' | 'disabled'; +} +export interface SetupProjectConfiguration { + ids: string; + issueCreatedColumn: string; + pullRequestCreatedColumn: string; + issueInProgressColumn: string; + pullRequestInProgressColumn: string; +} +export interface SetupConfiguration { + features: SetupFeatures; + agents: SetupAgentConfiguration; + repository: SetupRepositoryConfiguration; + ai: SetupAiConfiguration; + projects: SetupProjectConfiguration; + createInitialTag: boolean; + manageRepositoryVariables: boolean; + /** Whether setup should provision repository secrets after validating them. */ + manageRepositorySecrets: boolean; + /** Extra non-secret action inputs accepted by config files for advanced use cases. */ + actionInputs: Record; +} +export type SetupCredentialKind = 'workflowPat' | 'apiKey'; +export type SetupCredentialStatus = 'valid' | 'invalid' | 'missing' | 'unverifiable' | 'not_required'; +/** A credential requirement is metadata only; never put a secret value in this object. */ +export interface SetupCredentialRequirement { + name: string; + kind: SetupCredentialKind; + description: string; + provider?: string; + model?: string; +} +export interface SetupCredentialCheck { + name: string; + status: SetupCredentialStatus; + message: string; + account?: string; +} +export interface SetupCredentialValue { + name: string; + value: string; +} +export type SetupCredentialDecision = 'keep' | 'replace' | 'skip'; +export interface SetupCredentialCollection { + workflowPat?: SetupCredentialValue; + apiKeys: SetupCredentialValue[]; +} +export interface SetupWorkflowComparison { + file: string; + destination: string; + status: 'missing' | 'unchanged' | 'changed' | 'unmanaged'; +} +export type DoctorCheckStatus = 'pass' | 'warn' | 'fail'; +export interface DoctorCheck { + area: string; + status: DoctorCheckStatus; + message: string; +} +export interface SetupVariable { + name: string; + value: string; +} +export interface SetupPlan { + configuration: SetupConfiguration; + workflowFiles: string[]; + issueTemplateFiles: string[]; + selectedFiles: string[]; + variables: SetupVariable[]; + requiredSecrets: string[]; + credentialRequirements: SetupCredentialRequirement[]; + warnings: string[]; +} diff --git a/build/github_action/src/infrastructure/composition/github_identity_client_factory.d.ts b/build/github_action/src/infrastructure/composition/github_identity_client_factory.d.ts index 4d1183cd..64c35141 100644 --- a/build/github_action/src/infrastructure/composition/github_identity_client_factory.d.ts +++ b/build/github_action/src/infrastructure/composition/github_identity_client_factory.d.ts @@ -1,4 +1,6 @@ import { OctokitAuthenticatedUserClientAdapter, OctokitActorAuthorizationClientAdapter, OctokitOrganizationMembersClientAdapter } from "../github/octokit_identity_adapters"; +import { OctokitRepositoryVariablesClientAdapter } from '../github/octokit_repository_variables_adapter'; export declare const createAuthenticatedUserClient: () => OctokitAuthenticatedUserClientAdapter; export declare const createActorAuthorizationClient: () => OctokitActorAuthorizationClientAdapter; export declare const createOrganizationMembersClient: () => OctokitOrganizationMembersClientAdapter; +export declare const createRepositoryVariablesClient: () => OctokitRepositoryVariablesClientAdapter; diff --git a/build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts b/build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts new file mode 100644 index 00000000..0c0bf531 --- /dev/null +++ b/build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts @@ -0,0 +1,3 @@ +import { SetupCredentialsUseCase } from '../../application/usecases/setup/setup_credentials_use_case'; +import type { SetupCredentialPromptPort } from '../../application/ports/setup_wizard_ports'; +export declare function createSetupCredentialsUseCase(prompt: SetupCredentialPromptPort): SetupCredentialsUseCase; diff --git a/build/github_action/src/infrastructure/composition/setup_doctor_composition_root.d.ts b/build/github_action/src/infrastructure/composition/setup_doctor_composition_root.d.ts new file mode 100644 index 00000000..ea0418e1 --- /dev/null +++ b/build/github_action/src/infrastructure/composition/setup_doctor_composition_root.d.ts @@ -0,0 +1,3 @@ +import { SetupDoctorUseCase } from '../../application/usecases/setup/doctor_use_case'; +import type { DoctorOutputPort } from '../../application/ports/setup_wizard_ports'; +export declare function createSetupDoctorUseCase(output: DoctorOutputPort): SetupDoctorUseCase; diff --git a/build/github_action/src/infrastructure/github/octokit_credential_health_adapter.d.ts b/build/github_action/src/infrastructure/github/octokit_credential_health_adapter.d.ts new file mode 100644 index 00000000..07e458bb --- /dev/null +++ b/build/github_action/src/infrastructure/github/octokit_credential_health_adapter.d.ts @@ -0,0 +1,5 @@ +import type { GithubClientPort } from './ports/github_client_provider_port'; +import type { GithubCredentialHealthClient } from './ports/github_credential_health_protocol'; +export declare class OctokitCredentialHealthClientAdapter implements GithubClientPort { + getClient(token: string): GithubCredentialHealthClient; +} diff --git a/build/github_action/src/infrastructure/github/octokit_repository_variables_adapter.d.ts b/build/github_action/src/infrastructure/github/octokit_repository_variables_adapter.d.ts new file mode 100644 index 00000000..7d77d208 --- /dev/null +++ b/build/github_action/src/infrastructure/github/octokit_repository_variables_adapter.d.ts @@ -0,0 +1,5 @@ +import type { GithubClientPort } from './ports/github_client_provider_port'; +import type { GithubRepositoryVariablesClient } from './ports/github_repository_variables_protocol'; +export declare class OctokitRepositoryVariablesClientAdapter implements GithubClientPort { + getClient(token: string): GithubRepositoryVariablesClient; +} diff --git a/build/github_action/src/infrastructure/github/ports/github_credential_health_protocol.d.ts b/build/github_action/src/infrastructure/github/ports/github_credential_health_protocol.d.ts new file mode 100644 index 00000000..175b21ba --- /dev/null +++ b/build/github_action/src/infrastructure/github/ports/github_credential_health_protocol.d.ts @@ -0,0 +1,52 @@ +export interface GithubCredentialHealthClient { + rest: { + actions: { + createWorkflowDispatch(parameters: Record): Promise; + listWorkflowRuns(parameters: Record): Promise<{ + data: { + workflow_runs: GithubWorkflowRun[]; + }; + }>; + getWorkflowRun(parameters: Record): Promise<{ + data: GithubWorkflowRun; + }>; + listJobsForWorkflowRun(parameters: Record): Promise<{ + data: { + jobs: GithubWorkflowJob[]; + }; + }>; + getWorkflow(parameters: Record): Promise; + }; + }; + repos: { + get(parameters: Record): Promise<{ + data: { + default_branch?: string; + }; + }>; + getContent(parameters: Record): Promise<{ + data: { + sha?: string; + }; + }>; + createOrUpdateFileContents(parameters: Record): Promise<{ + data?: { + content?: { + sha?: string; + }; + }; + }>; + deleteFile(parameters: Record): Promise; + }; +} +export interface GithubWorkflowRun { + id: number; + status?: string | null; + conclusion?: string | null; + created_at?: string; +} +export interface GithubWorkflowJob { + name: string; + status?: string | null; + conclusion?: string | null; +} diff --git a/build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts b/build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts new file mode 100644 index 00000000..37799fed --- /dev/null +++ b/build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts @@ -0,0 +1,36 @@ +export interface GithubRepositoryVariable { + name: string; + value?: string; +} +export interface GithubRepositoryVariablesClient { + rest: { + actions: { + listRepoVariables(parameters: Record): Promise<{ + data: { + variables: GithubRepositoryVariable[]; + }; + }>; + createRepoVariable(parameters: Record): Promise; + updateRepoVariable(parameters: Record): Promise; + }; + secrets?: { + listRepoSecrets(parameters: Record): Promise<{ + data: { + secrets: GithubRepositorySecret[]; + }; + }>; + getRepoPublicKey(parameters: Record): Promise<{ + data: { + key_id: string; + key: string; + }; + }>; + createOrUpdateRepoSecret(parameters: Record): Promise; + }; + }; +} +export interface GithubRepositorySecret { + name: string; + created_at?: string; + updated_at?: string; +} diff --git a/build/github_action/src/infrastructure/setup_credential_validation_adapter.d.ts b/build/github_action/src/infrastructure/setup_credential_validation_adapter.d.ts new file mode 100644 index 00000000..c14b72c9 --- /dev/null +++ b/build/github_action/src/infrastructure/setup_credential_validation_adapter.d.ts @@ -0,0 +1,18 @@ +import type { SetupCredentialCheck, SetupCredentialRequirement } from '../domain/setup'; +import type { SetupCredentialValidationPort } from '../application/ports/setup_wizard_ports'; +export interface SetupCredentialValidationOptions { + fetcher?: typeof fetch; + timeoutMs?: number; +} +/** + * Performs bounded, metadata-only credential checks. Provider responses are + * intentionally never returned or logged because they can contain account data. + */ +export declare class SetupCredentialValidationAdapter implements SetupCredentialValidationPort { + private readonly fetcher; + private readonly timeoutMs; + constructor(options?: SetupCredentialValidationOptions); + validateSetupPat(owner: string, repository: string, token: string): Promise; + validateCredential(requirement: SetupCredentialRequirement, value: string): Promise; + private requestJson; +} diff --git a/build/github_action/src/infrastructure/setup_remote_credential_health_adapter.d.ts b/build/github_action/src/infrastructure/setup_remote_credential_health_adapter.d.ts new file mode 100644 index 00000000..71aa008a --- /dev/null +++ b/build/github_action/src/infrastructure/setup_remote_credential_health_adapter.d.ts @@ -0,0 +1,25 @@ +import type { SetupCredentialCheck, SetupCredentialRequirement } from '../domain/setup'; +import type { SetupRemoteCredentialHealthPort } from '../application/ports/setup_wizard_ports'; +import type { GithubClientPort } from './github/ports/github_client_provider_port'; +import type { GithubCredentialHealthClient } from './github/ports/github_credential_health_protocol'; +export interface CredentialHealthAdapterOptions { + waitMs?: number; + pollMs?: number; + sleep?: (milliseconds: number) => Promise; + bootstrapWhenMissing?: boolean; + workflowContent?: string; +} +/** Dispatches the repository-owned health workflow; it cannot read or mutate Secret values. */ +export declare class SetupRemoteCredentialHealthAdapter implements SetupRemoteCredentialHealthPort { + private readonly githubClient; + private readonly waitMs; + private readonly pollMs; + private readonly sleep; + private readonly bootstrapWhenMissing; + private readonly workflowContent; + constructor(githubClient: GithubClientPort, options?: CredentialHealthAdapterOptions); + validateExisting(owner: string, repository: string, token: string, ref: string, requirements: readonly SetupCredentialRequirement[]): Promise; + private bootstrapWorkflow; + private removeTemporaryWorkflow; + private findRun; +} diff --git a/build/github_action/src/infrastructure/setup_workspace_adapter.d.ts b/build/github_action/src/infrastructure/setup_workspace_adapter.d.ts index 37acfe08..0b5e361b 100644 --- a/build/github_action/src/infrastructure/setup_workspace_adapter.d.ts +++ b/build/github_action/src/infrastructure/setup_workspace_adapter.d.ts @@ -1,5 +1,7 @@ -import type { SetupWorkspacePort, SetupWorkspaceResult } from '../application/ports/setup_workspace_ports'; +import { compareSetupWorkflows } from '../utils/setup_files'; +import type { SetupWorkspacePort, SetupWorkspaceResult, SetupWorkspaceSelection } from '../application/ports/setup_workspace_ports'; export declare class SetupWorkspaceAdapter implements SetupWorkspacePort { - prepare(): SetupWorkspaceResult; - hasValidToken(): boolean; + prepare(selection?: SetupWorkspaceSelection): SetupWorkspaceResult; + hasValidToken(tokenOverride?: string): boolean; + compareWorkflows(features?: Parameters[1]): ReturnType; } diff --git a/build/github_action/src/utils/setup_file_copy.d.ts b/build/github_action/src/utils/setup_file_copy.d.ts index df53c574..15b2e382 100644 --- a/build/github_action/src/utils/setup_file_copy.d.ts +++ b/build/github_action/src/utils/setup_file_copy.d.ts @@ -2,5 +2,9 @@ export type CopyStats = { copied: number; skipped: number; }; -export declare function copySetupFile(source: string, destination: string, displaySource: string, displayDestination: string): CopyStats; -export declare function copySetupDirectory(sourceDirectory: string, destinationDirectory: string, fileFilter: (fileName: string) => boolean, displayDirectory: string): CopyStats; +export interface CopyOptions { + overwrite?: boolean; + backupDirectory?: string; +} +export declare function copySetupFile(source: string, destination: string, displaySource: string, displayDestination: string, options?: CopyOptions): CopyStats; +export declare function copySetupDirectory(sourceDirectory: string, destinationDirectory: string, fileFilter: (fileName: string) => boolean, displayDirectory: string, options?: CopyOptions): CopyStats; diff --git a/build/github_action/src/utils/setup_files.d.ts b/build/github_action/src/utils/setup_files.d.ts index 2fdbeeef..a96dab6a 100644 --- a/build/github_action/src/utils/setup_files.d.ts +++ b/build/github_action/src/utils/setup_files.d.ts @@ -1,3 +1,4 @@ +import type { SetupFeatures, SetupWorkflowComparison } from '../domain/setup'; /** * Ensure .github, .github/workflows and .github/ISSUE_TEMPLATE exist; create them if missing. * @param cwd - Directory (repo root) @@ -12,26 +13,23 @@ export declare function ensureGitHubDirs(cwd: string): void; * @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root. * @returns { copied, skipped } */ -export declare function copySetupFiles(cwd: string, setupDirOverride?: string): { +export declare function copySetupFiles(cwd: string, setupDirOverride?: string, features?: SetupFeatures, options?: { + updateExistingWorkflows?: boolean; + approvedWorkflowFiles?: readonly string[]; +}): { copied: number; skipped: number; }; -/** - * Logs the current state of PERSONAL_ACCESS_TOKEN (environment or .env). Does not create .env. - */ -export declare function ensureEnvWithToken(cwd: string): void; +export declare function compareSetupWorkflows(cwd: string, features?: SetupFeatures, setupDirOverride?: string): SetupWorkflowComparison[]; /** * Resolves the PERSONAL_ACCESS_TOKEN for setup from a single priority order: * 1. override (e.g. CLI --token) if provided and valid, - * 2. process.env.PERSONAL_ACCESS_TOKEN, - * 3. .env file in cwd. + * 2. process.env.PERSONAL_ACCESS_TOKEN. * Returns undefined if no valid token is found. */ -export declare function getSetupToken(cwd: string, override?: string): string | undefined; +export declare function getSetupToken(_cwd: string, override?: string): string | undefined; /** * Returns true if a valid setup token is available (same resolution order as getSetupToken). * Pass an optional override (e.g. CLI --token) so validation considers all sources consistently. */ export declare function hasValidSetupToken(cwd: string, override?: string): boolean; -/** Returns true if a .env file exists in the given directory. */ -export declare function setupEnvFileExists(cwd: string): boolean; diff --git a/docs/authentication.mdx b/docs/authentication.mdx index f06ba613..16b42867 100644 --- a/docs/authentication.mdx +++ b/docs/authentication.mdx @@ -3,9 +3,14 @@ title: Authentication description: Securely authenticate your workflows using personal access tokens (PATs). --- -Copilot requires a fine-grained personal access token to perform certain actions, such as creating branches, updating pull requests, or managing project boards. +Copilot uses two deliberately separate credentials: -Originally, the workflow also made use of the GITHUB_TOKEN for some basic tasks executed within the workflow's scope. However, to simplify the configuration and maintain a single unified bot, the use of this token has been removed, leaving only the necessary PAT. +- **Setup PAT (operator token):** entered in `copilot setup` or `copilot doctor`, used only for that local command, and never stored in the repository or in a GitHub Secret. The wizard validates it before making changes. +- **Workflow PAT (bot token):** owned by the bot account, stored as the repository or organization Secret `PAT`, and consumed by GitHub Actions at runtime. This is the token that gives the workflows their bot identity. + +The setup PAT and workflow PAT may have different owners and permissions. Do not paste the workflow PAT into the setup prompt unless you intentionally want the same token to perform both roles. + +GitHub does not reveal Secret values through its API. Copilot validates new credentials with provider metadata requests and validates existing remote Secrets through the read-only `copilot_credential_health.yml` workflow when it is installed on the repository's default branch. The health workflow reports each requested credential independently. **When the event actor is the same as the token user**: The action detects this before entering the workflow queue. It completes successfully without waiting or running the normal issue/PR/push pipeline. A valid explicit single action still runs. This avoids the bot reacting to its own actions. Use a dedicated bot account (different from the actor) if you want full pipeline behavior on every event. @@ -15,8 +20,14 @@ Copilot requires a fine-grained personal access token to perform certain actions - For individual developers, it is recommended to use your own account as the bot. - In organizations and enterprise accounts, it is better to use a separate, dedicated account. For example, this project belongs to [**vypdev**](https://github.com/vypdev), and the selected bot account is [**vypbot**](https://github.com/vypbot). + + + The person running setup needs a separate fine-grained PAT. Give it only the permissions required by the selected setup features: repository Metadata read and repository Contents/Workflows read for inspection; Issues write for labels; Variables write for Repository Variables; Secrets read/write when provisioning Secrets; Actions read/write when checking or dispatching credential health; and organization Issue Types or Projects permissions only when those integrations are selected. Contents write and Workflows write are required only if the operator chooses to modify workflow files through the GitHub API. + + Enter it in the hidden prompt, or use `--token`/`PERSONAL_ACCESS_TOKEN` for automation. It remains in memory for the command and is not written to `.env`, a config file, or the `PAT` Secret. + - + Once you’ve selected the account that will act as the bot: - Go to [**Settings**](https://github.com/settings/profile). - Navigate to [**Developer settings**](https://github.com/settings/apps). @@ -42,7 +53,7 @@ Copilot requires a fine-grained personal access token to perform certain actions - **Metadata**: Read-only - **Pull requests**: Read and write - **Secrets**: Read-only - - **Variables**: Read-only + - **Variables**: Read and write - **Webhooks**: Read and write - **Workflows**: Read and write @@ -52,24 +63,24 @@ Copilot requires a fine-grained personal access token to perform certain actions - **Issue Types**: Read and write - **Members**: Read-only - **Projects**: Admin - - **Secrets**: Read-only + - **Secrets**: Read-only unless the workflow itself must administer Secrets - **Self-hosted runners**: Read and write - - **Variables**: Read-only + - **Variables**: Read and write Finally press the **Generate new token** button. Make sure to **copy the generated PAT**, as it will not be visible again. - - It’s time to create a new Secret: + + It’s time to create the `PAT` Secret used by the workflows. The interactive setup can validate and create/update it directly when the setup PAT has Secrets write permission. Otherwise create it manually: If your bot account does **not** belong to an organization (individual developer): - Go to the repository where you want to implement Copilot. - Then, navigate to **Settings**. - In the left sidebar, click on **Secrets and variables**, then **Actions**. - Click **New repository secret**. - - Define a name for the secret and paste the previously created PAT in the **Secret** field. + - Define the name as `PAT` and paste the workflow PAT in the **Secret** field. - Finally, click **Add secret**. If your bot account **does** belong to an organization: diff --git a/docs/bugbot/detection.mdx b/docs/bugbot/detection.mdx index 479e1301..7b46aae1 100644 --- a/docs/bugbot/detection.mdx +++ b/docs/bugbot/detection.mdx @@ -40,7 +40,7 @@ issue. These commands do not invoke autofix and do not modify files. You can run Bugbot detection **without pushing**: - **Single action:** In a workflow, set `single-action: detect_potential_problems_action` and `single-action-issue: `. The workflow must run in a context where the **branch** to analyze is the current checkout (e.g. trigger on `workflow_dispatch` after checking out the branch you want to analyze). -- **CLI:** From the repository root, run `copilot detect-potential-problems -i ` (optionally `-b `). Requires a `.env` with `PERSONAL_ACCESS_TOKEN` and the configured agent CLI credentials/model. See [Examples → CLI](/bugbot/examples#cli) and [Smoke tests](/security-operations/operations/smoke-tests). +- **CLI:** From the repository root, run `copilot detect-potential-problems -i ` (optionally `-b `). Provide `--token` or `PERSONAL_ACCESS_TOKEN` in the environment, plus the configured agent CLI credentials/model. See [Examples → CLI](/bugbot/examples#cli) and [Smoke tests](/security-operations/operations/smoke-tests). In both cases, the action uses the same detection flow: load context for that issue/branch, call the configured agent, filter and apply comment limit, then publish to the issue and to any open PR for that branch. diff --git a/docs/bugbot/examples.mdx b/docs/bugbot/examples.mdx index c8fbae10..6c6da028 100644 --- a/docs/bugbot/examples.mdx +++ b/docs/bugbot/examples.mdx @@ -161,7 +161,7 @@ After the run, findings appear on the issue and on any open PR for that branch. ## CLI: detect potential problems -From the **repository root** (with a `.env` that has `PERSONAL_ACCESS_TOKEN` and, if needed, agent CLI environment variables), you can run Bugbot detection locally: +From the **repository root** (with `PERSONAL_ACCESS_TOKEN` in the environment and, if needed, agent CLI environment variables), you can run Bugbot detection locally: ```bash # Require issue number; optional branch (default: current branch) @@ -170,7 +170,7 @@ copilot detect-potential-problems -i 123 # Specify branch explicitly copilot detect-potential-problems -i 123 -b feature/issue-123 -# With token and debug (if not in .env) +# With an explicit token and debug copilot detect-potential-problems -i 123 -t $PAT -d ``` diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index 2664afbe..42f83ffb 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -17,6 +17,13 @@ The repository separates semantic application ports from provider-specific adapt The application layer MUST NOT import GitHub SDKs, concrete CLIs, process libraries, or provider-specific protocols. Provider adapters MUST NOT define business policy. Configuration validation belongs at the boundary before execution. Prompt construction, untrusted-content handling, and GitHub publication sanitization are separate policies so no agent capability can bypass the security boundary. +Setup follows the same boundary: credential collection, workflow comparison, +approval, and doctor decisions live in application use cases and semantic ports. +GitHub Secret encryption, workflow dispatch, provider metadata requests, and the +temporary health-workflow bootstrap are infrastructure adapters. Secret values +never enter setup override files, Variables, logs, or the generated workflow +templates. + ## Workflow queue boundary The repository-wide mutation queue is an application use case backed by semantic diff --git a/docs/development/local-development.mdx b/docs/development/local-development.mdx index 616104f5..b42bba99 100644 --- a/docs/development/local-development.mdx +++ b/docs/development/local-development.mdx @@ -4,6 +4,6 @@ description: Reproducible local workflow for changing Copilot. --- # Local development -Use Node and Corepack with the repository's pnpm version. Install dependencies with pnpm only. Keep provider credentials outside the repository and never commit local `.env` files. +Use Node and Corepack with the repository's pnpm version. Install dependencies with pnpm only. Keep provider credentials outside the repository and use the interactive setup prompt or environment variables; Copilot does not read or create `.env` files. Before editing, inspect `git status`. After editing, run focused tests, the full gates, the documentation validator, and `git diff --check`. Real provider calls require separate authorization and MUST use read-only prompts unless the task explicitly authorizes mutation. diff --git a/docs/development/testing.mdx b/docs/development/testing.mdx index 1410414e..ceea8ee8 100644 --- a/docs/development/testing.mdx +++ b/docs/development/testing.mdx @@ -52,6 +52,10 @@ resolution, provider credential isolation, and the language capability's separate application port. A passing model smoke test does not replace these deterministic tests. +Setup tests also cover the setup/workflow PAT separation, provider validation, +per-credential remote health results, invalid-secret replacement, workflow update +approval and backups, `.env` exclusion, and the read-only doctor path. + The reproducible `metrics:architecture` collector is the full evidence workflow; it requires a clean working tree and writes reports outside the repository. diff --git a/docs/how-to-use.mdx b/docs/how-to-use.mdx index e200153c..2488de40 100644 --- a/docs/how-to-use.mdx +++ b/docs/how-to-use.mdx @@ -30,7 +30,7 @@ corepack pnpm install . --global If the checkout does not include the compiled `build/` folder (e.g. it is gitignored), run `corepack pnpm install` and `corepack pnpm run build` before `corepack pnpm install . --global`. -Once installed, the `copilot` command is available globally. **All Copilot CLI commands** (including `copilot setup`, `copilot check-progress -i 123`, etc.) must be run **from inside the repository** where you want Copilot to run. Commands that access GitHub require **`PERSONAL_ACCESS_TOKEN`** in the environment or in a **`.env`** file at the repository root; `copilot setup` can copy static setup files before a token is available. See [CLI commands](/single-actions/workflow-and-cli). +Once installed, the `copilot` command is available globally. **All Copilot CLI commands** (including `copilot setup`, `copilot doctor`, and `copilot check-progress -i 123`) must be run **from inside the repository** where you want Copilot to run. Commands that access GitHub accept `--token` or `PERSONAL_ACCESS_TOKEN` from the environment. `copilot setup` and `copilot doctor` securely prompt for the setup PAT when run interactively; no `.env` file is read or created. `copilot setup --dry-run` is the only setup mode that can run without a token. See [CLI commands](/single-actions/workflow-and-cli). If you previously installed Copilot from a local checkout, installing the npm package switches the same `copilot` command to the published package. Check which executable and package are active: @@ -92,8 +92,7 @@ The complete command reference, including every supported option, is in [Workflo - The action needs a **Personal Access Token** to manage issues, branches, projects, and workflows. You must create it manually; `copilot setup` does not create it. - Follow the [Authentication](/authentication) guide to create a **fine-grained PAT** with the right permissions (Actions, Administration, Contents, Issues, Metadata, Pull requests, etc.) and store it as a secret (e.g. `PAT`) in your repository or organization. + Create the **workflow PAT** for the dedicated bot account. Follow the [Authentication](/authentication) guide to create a **fine-grained PAT** with the runtime permissions required by your workflows and store it as the `PAT` Secret in your repository or organization. During `copilot setup`, the operator will provide a separate **setup PAT** in a hidden prompt; that token is used locally to configure the repository and is never stored as `PAT`. @@ -104,25 +103,27 @@ The complete command reference, including every supported option, is in [Workflo copilot setup ``` - **About the token the first time:** - The first time you run `copilot setup`, there will usually be **no `.env` file** in that repo with the PAT. The command will copy setup files (workflows, templates, etc.) but may fail or skip the step that needs GitHub access (labels, issue types). In that case: + Before applying the plan, the wizard securely asks for the setup PAT. For automation, pass it explicitly or through the environment: - - Create a **`.env`** file in the **repository root** with your token: - ```bash - PERSONAL_ACCESS_TOKEN=your_fine_grained_token_here - ``` - - Run **`copilot setup` again**. This time it will have access to the repository and will create labels and issue types. + ```bash + PERSONAL_ACCESS_TOKEN=your_setup_pat copilot setup --non-interactive --yes --skip-secrets + # or: copilot setup --token your_setup_pat + ``` - Alternatively, you can create the `.env` file with `PERSONAL_ACCESS_TOKEN` and its value **before** the first run and execute `copilot setup` once; it will then complete all steps (copy files + create labels and issue types) in a single pass. + The wizard shows a reviewable plan and asks for confirmation. Use `copilot setup --dry-run` to inspect it without a token or changes. `copilot setup` will: - Create `.github/`, `.github/workflows/`, and `.github/ISSUE_TEMPLATE/` if they do not exist. - - Copy all files from the Copilot `setup/` folder into your repo (workflows, issue templates, pull request template). Existing files are **not** overwritten. - - Verify GitHub access using `PERSONAL_ACCESS_TOKEN` from `.env`, the environment, or the `--token` option. + - Copy only the selected files from the Copilot `setup/` folder into your repo (workflows, issue templates, pull request template). Existing files are **not** overwritten unless you approve an update. + - Detect existing setup workflows and ask before updating them. Use `--update-workflows` to approve updates non-interactively; existing workflow files are backed up under `.copilot/setup-backups/` before replacement. + - Verify GitHub access using the setup PAT. This operator token is used only for the setup operation and is never stored as a workflow Secret. + - Ask for the separate workflow PAT owned by the bot account. It is validated and stored remotely as the `PAT` Secret; it is not the setup PAT. + - Ask for each selected provider API key, validate it with a metadata-only provider request, and store it as a GitHub Secret. Existing Secrets are never overwritten unless you explicitly replace them. If a remote credential health workflow is already available, existing values are validated remotely before you choose to keep them. + - Create or update non-sensitive Repository Variables for the selected runtimes, model policy, branches, Projects, AI behavior (including `AI_IGNORE_FILES`), and Bugbot settings. Use `--skip-variables` to leave them unchanged. - Create all required **labels** in the repository (type, action, priority, size, progress 0%–100%, and the lifecycle/activity/waiting labels). - Create all required **issue types** in the organization (Task, Bug, Feature, Release, Hotfix, etc.), if your plan supports it. - After this step you have a working baseline: labels, templates, and workflow files are in place. + After this step you have a working baseline: labels, selected templates/workflows, agent routing, operational Variables, and validated credentials are in place. @@ -149,7 +150,7 @@ The complete command reference, including every supported option, is in [Workflo ## What lives in `setup/` (and what gets created) -The workflow and template files under `setup/` are copied into your repo by `copySetupFiles` (run during `copilot setup`). The `setup/.env` file is a local, developer-friendly credential-key template and is intentionally not copied. Below is a desglose of labels (with defaults), issue types, and the contents of `setup/` so you know what you can customize and what must stay consistent. +The workflow and template files under `setup/` are copied into your repo by `copilot setup`. Credentials are never shipped in the package or written to local configuration files. Below is a breakdown of labels (with defaults), issue types, and the contents of `setup/` so you know what you can customize and what must stay consistent. ### Coherence when customizing @@ -304,9 +305,9 @@ The **labels** in each template must match the label names configured in the act Copied to `.github/pull_request_template.md`. Used as the default body for new PRs. The AI PR description feature can fill this structure; you can edit the sections (Summary, Related Issues, Scope, Technical Details, How to Test, etc.) to fit your repo. No Copilot logic depends on specific headings; only the deploy/release/hotfix flows depend on **workflow filenames** and **label names**. -### `setup/.env` +### `setup/workflows/copilot_credential_health.yml` -The setup package keeps a developer-friendly `setup/.env` template containing only `PERSONAL_ACCESS_TOKEN=` and no value. For local CLI usage, create an ignored `.env` file in the root of the target repository and set the token there. This file is not used by the GitHub Action; workflow credentials come from GitHub Secrets. +This manual workflow verifies selected remote credentials without printing or returning their values. `copilot doctor` dispatches it when the workflow is present on the repository's default branch. GitHub only exposes Secret names through its API, so this workflow is required to verify the value of an existing Secret. --- diff --git a/docs/single-actions/examples.mdx b/docs/single-actions/examples.mdx index 8818f643..9d868252 100644 --- a/docs/single-actions/examples.mdx +++ b/docs/single-actions/examples.mdx @@ -157,7 +157,7 @@ Often run once per repo or after adding new label/type config. On new repos, run ## CLI examples -Run the CLI from the **target repository root** (with `.env` containing `PERSONAL_ACCESS_TOKEN` and optional agent runtime variables). The first five commands mirror single actions; `copilot do` is CLI-only. See [Workflow & CLI](/single-actions/workflow-and-cli) for installation, updates, and the complete option reference. +Run the CLI from the **target repository root**. `copilot setup` and `copilot doctor` prompt for the setup PAT; other commands accept `--token` or `PERSONAL_ACCESS_TOKEN` from the environment. The first five commands mirror single actions; `copilot do` is CLI-only. See [Workflow & CLI](/single-actions/workflow-and-cli) for installation, updates, and the complete option reference. ### setup diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index 5709431a..171eb6b5 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -105,17 +105,10 @@ Run every command from the **root of the target repository**, not necessarily fr - be a Git worktree; - have an `origin` remote pointing to a GitHub repository; -- have `PERSONAL_ACCESS_TOKEN` available through the environment or a `.env` file in its root for commands that access GitHub; +- have `PERSONAL_ACCESS_TOKEN` available through the environment, or pass `--token` to commands that support it; - have the selected agent runtime and credentials configured for AI commands. -For local use, prefer a `.env` file or an environment variable instead of putting a token in shell history: - -```bash -cd /path/to/target-repository -printf 'PERSONAL_ACCESS_TOKEN=your_fine_grained_token_here\n' > .env -``` - -Keep `.env` ignored and never commit it. The commands that access GitHub accept `-t, --token` as an explicit alternative; `copilot do` does not use that option and reads the configured agent environment instead. See [Authentication](/authentication) and [Agent CLI configuration](/agents/cli-configuration). +For local use, prefer the hidden prompt or an environment variable instead of putting a token in shell history. `copilot setup` and `copilot doctor` prompt for the setup PAT; other commands accept `-t, --token` or `PERSONAL_ACCESS_TOKEN`. `copilot do` does not use a GitHub PAT option and reads the configured agent environment instead. See [Authentication](/authentication) and [Agent CLI configuration](/agents/cli-configuration). ## Command reference @@ -123,18 +116,95 @@ All commands support `-h, --help`. The `-d, --debug` option enables additional d ### `copilot setup` -Initializes the current GitHub repository by copying workflows and templates, verifying access, and creating the configured labels and issue types. If the repository has no version tags, setup also creates the default `v1.0.0` tag. +Initializes the current GitHub repository through an interactive English-language wizard. It can select the workflows and templates to install, route each Copilot task to Codex, OpenCode, or Cursor, configure repository behavior and AI policy, upsert non-sensitive GitHub Repository Variables, verify access, and create the configured labels and issue types. If the repository has no version tags, setup asks whether it should create `v1.0.0`. | Option | Required | Description | | --- | --- | --- | -| `-t, --token ` | No | PAT override. Otherwise uses `PERSONAL_ACCESS_TOKEN` from the environment or `.env`. | +| `-t, --token ` | No | Setup PAT override. Otherwise uses the hidden prompt interactively or `PERSONAL_ACCESS_TOKEN` from the environment. | | `-d, --debug` | No | Enables debug logging. | +| `--agent ` | No | Preselect one runtime for every task: `codex`, `opencode`, or `cursor`. | +| `--features ` | No | Preselect a comma-separated feature list, or `all`. Feature names are listed below. | +| `--config ` | No | Load non-secret YAML/JSON setup overrides. Interactive answers can still refine them. | +| `--non-interactive` | No | Use defaults, flags, and config-file values without prompts. | +| `--yes` | No | Skip the final confirmation prompt. | +| `--dry-run` | No | Print the complete plan without changing files or GitHub. A token is not required. | +| `--skip-variables` | No | Copy files and provision metadata without changing Repository Variables. | +| `--skip-secrets` | No | Do not validate or create/update repository Secrets. | +| `--update-workflows` | No | Approve updates to changed setup workflows already present in the repository. | +| `--workflow-pat ` | No | Workflow PAT for non-interactive setup; prefer the hidden prompt. | +| `--secret ` | No | Repeat for provider credentials in non-interactive setup; values can appear in shell history. | ```bash copilot setup ``` -Run it from the target repository root. Existing setup files are not overwritten. +Run it from the target repository root. Existing setup files are not overwritten unless you approve the update prompt or pass `--update-workflows`. The interactive flow installs the selected workflows/templates, asks for agent routing and operational settings, validates the separate workflow PAT and selected provider credentials, and creates or updates the required remote Secrets and Variables. + +The wizard covers these features: `issues`, `pullRequests`, `commits`, `issueComments`, `pullRequestComments`, `release`, `hotfix`, `agentProvisioning`, `credentialHealth`, `issueTemplates`, and `pullRequestTemplate`. The agent tasks are `planner`, `findings`, `reviewer`, `fixer`, `tester`, and `release`; each can use any of the three supported runtimes independently. Model provider, model, effort, branch strategy, locales, AI ignore patterns, project columns, Bugbot policy, provisioning mode, and initial-tag creation are also configurable. Cursor is available as an experimental runtime and is called out in the review plan with its extra credential/checksum requirements. + +For automation, use the same defaults without prompts: + +```bash +copilot setup --non-interactive --yes +copilot setup --dry-run +copilot setup --non-interactive --features issues,pullRequests,commits,issueComments,pullRequestComments --agent codex +``` + +For unattended credential provisioning, keep the setup PAT in a protected CI secret and pass credential values explicitly (never commit them): + +```bash +copilot setup --non-interactive --yes --update-workflows \ + --token "$SETUP_PAT" \ + --workflow-pat "$WORKFLOW_PAT" \ + --secret "OPENAI_API_KEY=$OPENAI_API_KEY" +``` + +The explicit `--workflow-pat` and `--secret` options are provided for automation and can appear in process listings or shell history. The interactive hidden prompt is safer for a human operator. + +### `copilot doctor` + +Checks the setup PAT, selected workflow templates, Repository Variables, Secret presence, and remote credential health. It never creates, updates, deletes, or overwrites configuration. The health workflow may create a normal `workflow_dispatch` run, but it does not change repository configuration or expose Secret values. + +```bash +copilot doctor +copilot doctor --token "$SETUP_PAT" +copilot doctor --config .copilot-setup.yml --non-interactive +``` + +Existing GitHub Secret values cannot be read through the GitHub API. `copilot doctor` therefore dispatches `.github/workflows/copilot_credential_health.yml` when it is installed on the repository default branch. Each requested credential runs in its own job, so one invalid key does not make unrelated valid keys appear invalid. A present Secret without that workflow is reported as present but unverifiable; custom provider credentials are also reported as unverifiable until a provider-specific health check is available. + +An override file can contain only non-secret values: + +```yaml +features: + release: false + hotfix: false +agents: + planner: + provider: codex + modelProvider: openai + model: gpt-5.6-luna + reviewer: + provider: opencode + modelProvider: anthropic + model: claude-3-7-sonnet + effort: high +repository: + mainBranch: main + developmentBranch: develop +projects: + ids: PVT_kwDOExample + issueCreatedColumn: Todo + issueInProgressColumn: In Progress +ai: + bugbotSeverity: medium + bugbotCommentLimit: 10 + ignoreFiles: node_modules/*,build/*,dist/* +createInitialTag: true +manageRepositoryVariables: true +``` + +Run it with `copilot setup --config .copilot-setup.yml`. The wizard rejects values that look like tokens, API keys, passwords, or other credential material. Secret values are accepted only through the hidden prompt or explicit CLI inputs and are written directly to GitHub Secrets after validation; they are never written to repository files or Variables. The required secret names are shown in the final plan; normally they include `PAT` plus the credentials needed by the selected runtime/model providers. ### `copilot check-progress` diff --git a/package.json b/package.json index d173add9..42f81606 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@actions/github": "^6.0.1", "@types/axios": "^0.14.4", "@types/chance": "^1.1.6", - "@types/dotenv": "^8.2.3", "@types/js-yaml": "^4.0.9", "axios": "^1.8.4", "boxen": "^8.0.1", @@ -67,9 +66,9 @@ "chance": "^1.1.12", "commander": "^12.0.0", "dockerode": "^4.0.5", - "dotenv": "^16.5.0", "js-yaml": "^4.1.0", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.3", + "tweetnacl": "^1.0.3" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 184bcf68..543c92d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,7 @@ importers: dependencies: '@actions/cache': specifier: ^4.1.0 - version: 4.1.0 + version: 4.1.0(supports-color@8.1.1) '@actions/core': specifier: ^1.11.1 version: 1.11.1 @@ -26,19 +26,16 @@ importers: version: 6.0.1 '@types/axios': specifier: ^0.14.4 - version: 0.14.4 + version: 0.14.4(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) '@types/chance': specifier: ^1.1.6 version: 1.1.8 - '@types/dotenv': - specifier: ^8.2.3 - version: 8.2.3 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 axios: specifier: ^1.8.4 - version: 1.19.0 + version: 1.19.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) boxen: specifier: ^8.0.1 version: 8.0.1 @@ -53,16 +50,16 @@ importers: version: 12.1.0 dockerode: specifier: ^4.0.5 - version: 4.0.12 - dotenv: - specifier: ^16.5.0 - version: 16.6.1 + version: 4.0.12(supports-color@8.1.1) js-yaml: specifier: ^4.1.0 version: 4.3.1 shell-quote: specifier: ^1.8.3 version: 1.10.0 + tweetnacl: + specifier: ^1.0.3 + version: 1.0.3 devDependencies: '@eslint/js': specifier: ^9.15.0 @@ -81,19 +78,19 @@ importers: version: 0.36.1 eslint: specifier: ^9.15.0 - version: 9.39.5 + version: 9.39.5(supports-color@8.1.1) jest: specifier: ^30.2.0 - version: 30.4.2(@types/node@22.20.1) + version: 30.4.2(@types/node@22.20.1)(supports-color@8.1.1) ts-jest: specifier: ^29.4.5 - version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@22.20.1))(typescript@5.9.3) + version: 29.4.12(@babel/core@7.29.7(supports-color@8.1.1))(@jest/transform@30.4.1(supports-color@8.1.1))(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(jest-util@30.4.1)(jest@30.4.2(@types/node@22.20.1)(supports-color@8.1.1))(typescript@5.9.3) typescript: specifier: ^5.2.2 version: 5.9.3 typescript-eslint: specifier: ^8.15.0 - version: 8.67.0(eslint@9.39.5)(typescript@5.9.3) + version: 8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) packages: @@ -679,10 +676,6 @@ packages: '@types/dockerode@3.3.47': resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} - '@types/dotenv@8.2.3': - resolution: {integrity: sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==} - deprecated: This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed. - '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -827,51 +820,61 @@ packages: resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.12.2': resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} cpu: [loong64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-loong64-musl@1.12.2': resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} cpu: [loong64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.12.2': resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.12.2': resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-openharmony-arm64@1.12.2': resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} @@ -1196,10 +1199,6 @@ packages: resolution: {integrity: sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==} engines: {node: '>= 8.0'} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1276,6 +1275,7 @@ packages: eslint@9.39.5: resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -2194,6 +2194,9 @@ packages: tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2340,7 +2343,7 @@ packages: snapshots: - '@actions/cache@4.1.0': + '@actions/cache@4.1.0(supports-color@8.1.1)': dependencies: '@actions/core': 1.11.1 '@actions/exec': 1.1.1 @@ -2348,8 +2351,8 @@ snapshots: '@actions/http-client': 2.2.3 '@actions/io': 1.1.3 '@azure/abort-controller': 1.1.0 - '@azure/ms-rest-js': 2.7.0 - '@azure/storage-blob': 12.33.0 + '@azure/ms-rest-js': 2.7.0(supports-color@8.1.1) + '@azure/storage-blob': 12.33.0(supports-color@8.1.1) '@protobuf-ts/runtime-rpc': 2.11.1 semver: 6.3.1 transitivePeerDependencies: @@ -2395,37 +2398,37 @@ snapshots: dependencies: tslib: 2.8.1 - '@azure/core-auth@1.11.0': + '@azure/core-auth@1.11.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.2.0 - '@azure/core-util': 1.14.0 + '@azure/core-util': 1.14.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-client@1.11.0': + '@azure/core-client@1.11.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.2.0 - '@azure/core-auth': 1.11.0 - '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-auth': 1.11.0(supports-color@8.1.1) + '@azure/core-rest-pipeline': 1.25.0(supports-color@8.1.1) '@azure/core-tracing': 1.4.0 - '@azure/core-util': 1.14.0 - '@azure/logger': 1.4.0 + '@azure/core-util': 1.14.0(supports-color@8.1.1) + '@azure/logger': 1.4.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-http-compat@2.5.0(@azure/core-client@1.11.0)(@azure/core-rest-pipeline@1.25.0)': + '@azure/core-http-compat@2.5.0(@azure/core-client@1.11.0(supports-color@8.1.1))(@azure/core-rest-pipeline@1.25.0(supports-color@8.1.1))': dependencies: '@azure/abort-controller': 2.2.0 - '@azure/core-client': 1.11.0 - '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-client': 1.11.0(supports-color@8.1.1) + '@azure/core-rest-pipeline': 1.25.0(supports-color@8.1.1) - '@azure/core-lro@2.7.2': + '@azure/core-lro@2.7.2(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.2.0 - '@azure/core-util': 1.14.0 - '@azure/logger': 1.4.0 + '@azure/core-util': 1.14.0(supports-color@8.1.1) + '@azure/logger': 1.4.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -2434,14 +2437,14 @@ snapshots: dependencies: tslib: 2.8.1 - '@azure/core-rest-pipeline@1.25.0': + '@azure/core-rest-pipeline@1.25.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.2.0 - '@azure/core-auth': 1.11.0 + '@azure/core-auth': 1.11.0(supports-color@8.1.1) '@azure/core-tracing': 1.4.0 - '@azure/core-util': 1.14.0 - '@azure/logger': 1.4.0 - '@typespec/ts-http-runtime': 0.3.8 + '@azure/core-util': 1.14.0(supports-color@8.1.1) + '@azure/logger': 1.4.0(supports-color@8.1.1) + '@typespec/ts-http-runtime': 0.3.8(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -2450,10 +2453,10 @@ snapshots: dependencies: tslib: 2.8.1 - '@azure/core-util@1.14.0': + '@azure/core-util@1.14.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.2.0 - '@typespec/ts-http-runtime': 0.3.8 + '@typespec/ts-http-runtime': 0.3.8(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -2463,16 +2466,16 @@ snapshots: fast-xml-parser: 5.10.1 tslib: 2.8.1 - '@azure/logger@1.4.0': + '@azure/logger@1.4.0(supports-color@8.1.1)': dependencies: - '@typespec/ts-http-runtime': 0.3.8 + '@typespec/ts-http-runtime': 0.3.8(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/ms-rest-js@2.7.0': + '@azure/ms-rest-js@2.7.0(supports-color@8.1.1)': dependencies: - '@azure/core-auth': 1.11.0 + '@azure/core-auth': 1.11.0(supports-color@8.1.1) abort-controller: 3.0.0 form-data: 2.5.6 node-fetch: 2.7.0 @@ -2484,34 +2487,34 @@ snapshots: - encoding - supports-color - '@azure/storage-blob@12.33.0': + '@azure/storage-blob@12.33.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.2.0 - '@azure/core-auth': 1.11.0 - '@azure/core-client': 1.11.0 - '@azure/core-http-compat': 2.5.0(@azure/core-client@1.11.0)(@azure/core-rest-pipeline@1.25.0) - '@azure/core-lro': 2.7.2 + '@azure/core-auth': 1.11.0(supports-color@8.1.1) + '@azure/core-client': 1.11.0(supports-color@8.1.1) + '@azure/core-http-compat': 2.5.0(@azure/core-client@1.11.0(supports-color@8.1.1))(@azure/core-rest-pipeline@1.25.0(supports-color@8.1.1)) + '@azure/core-lro': 2.7.2(supports-color@8.1.1) '@azure/core-paging': 1.7.0 - '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-rest-pipeline': 1.25.0(supports-color@8.1.1) '@azure/core-tracing': 1.4.0 - '@azure/core-util': 1.14.0 + '@azure/core-util': 1.14.0(supports-color@8.1.1) '@azure/core-xml': 1.6.0 - '@azure/logger': 1.4.0 - '@azure/storage-common': 12.5.0(@azure/core-client@1.11.0) + '@azure/logger': 1.4.0(supports-color@8.1.1) + '@azure/storage-common': 12.5.0(@azure/core-client@1.11.0(supports-color@8.1.1))(supports-color@8.1.1) events: 3.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/storage-common@12.5.0(@azure/core-client@1.11.0)': + '@azure/storage-common@12.5.0(@azure/core-client@1.11.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.2.0 - '@azure/core-auth': 1.11.0 - '@azure/core-http-compat': 2.5.0(@azure/core-client@1.11.0)(@azure/core-rest-pipeline@1.25.0) - '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-auth': 1.11.0(supports-color@8.1.1) + '@azure/core-http-compat': 2.5.0(@azure/core-client@1.11.0(supports-color@8.1.1))(@azure/core-rest-pipeline@1.25.0(supports-color@8.1.1)) + '@azure/core-rest-pipeline': 1.25.0(supports-color@8.1.1) '@azure/core-tracing': 1.4.0 - '@azure/core-util': 1.14.0 - '@azure/logger': 1.4.0 + '@azure/core-util': 1.14.0(supports-color@8.1.1) + '@azure/logger': 1.4.0(supports-color@8.1.1) events: 3.3.0 tslib: 2.8.1 transitivePeerDependencies: @@ -2526,20 +2529,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@8.1.1) '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2564,19 +2567,19 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@8.1.1) '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -2597,89 +2600,89 @@ snapshots: dependencies: '@babel/types': 7.29.8 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/template@7.29.7': @@ -2688,7 +2691,7 @@ snapshots: '@babel/parser': 7.29.8 '@babel/types': 7.29.8 - '@babel/traverse@7.29.8': + '@babel/traverse@7.29.8(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 @@ -2696,7 +2699,7 @@ snapshots: '@babel/parser': 7.29.8 '@babel/template': 7.29.7 '@babel/types': 7.29.8 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -2725,17 +2728,17 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)': + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(supports-color@8.1.1))': dependencies: - eslint: 9.39.5 + eslint: 9.39.5(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.2': + '@eslint/config-array@0.21.2(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -2748,10 +2751,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.6': + '@eslint/eslintrc@3.3.6(supports-color@8.1.1)': dependencies: ajv: 6.15.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -2834,13 +2837,13 @@ snapshots: jest-util: 30.4.1 slash: 3.0.0 - '@jest/core@30.4.2': + '@jest/core@30.4.2(supports-color@8.1.1)': dependencies: '@jest/console': 30.4.1 '@jest/pattern': 30.4.0 - '@jest/reporters': 30.4.1 + '@jest/reporters': 30.4.1(supports-color@8.1.1) '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 + '@jest/transform': 30.4.1(supports-color@8.1.1) '@jest/types': 30.4.1 '@types/node': 22.20.1 ansi-escapes: 4.3.2 @@ -2850,15 +2853,15 @@ snapshots: fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 jest-changed-files: 30.4.1 - jest-config: 30.4.2(@types/node@22.20.1) + jest-config: 30.4.2(@types/node@22.20.1)(supports-color@8.1.1) jest-haste-map: 30.4.1 jest-message-util: 30.4.1 jest-regex-util: 30.4.0 jest-resolve: 30.4.1 - jest-resolve-dependencies: 30.4.2 - jest-runner: 30.4.2 - jest-runtime: 30.4.2 - jest-snapshot: 30.4.1 + jest-resolve-dependencies: 30.4.2(supports-color@8.1.1) + jest-runner: 30.4.2(supports-color@8.1.1) + jest-runtime: 30.4.2(supports-color@8.1.1) + jest-snapshot: 30.4.1(supports-color@8.1.1) jest-util: 30.4.1 jest-validate: 30.4.1 jest-watcher: 30.4.1 @@ -2883,10 +2886,10 @@ snapshots: dependencies: '@jest/get-type': 30.1.0 - '@jest/expect@30.4.1': + '@jest/expect@30.4.1(supports-color@8.1.1)': dependencies: expect: 30.4.1 - jest-snapshot: 30.4.1 + jest-snapshot: 30.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -2901,10 +2904,10 @@ snapshots: '@jest/get-type@30.1.0': {} - '@jest/globals@30.4.1': + '@jest/globals@30.4.1(supports-color@8.1.1)': dependencies: '@jest/environment': 30.4.1 - '@jest/expect': 30.4.1 + '@jest/expect': 30.4.1(supports-color@8.1.1) '@jest/types': 30.4.1 jest-mock: 30.4.1 transitivePeerDependencies: @@ -2915,12 +2918,12 @@ snapshots: '@types/node': 22.20.1 jest-regex-util: 30.4.0 - '@jest/reporters@30.4.1': + '@jest/reporters@30.4.1(supports-color@8.1.1)': dependencies: '@bcoe/v8-coverage': 0.2.3 '@jest/console': 30.4.1 '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 + '@jest/transform': 30.4.1(supports-color@8.1.1) '@jest/types': 30.4.1 '@jridgewell/trace-mapping': 0.3.31 '@types/node': 22.20.1 @@ -2930,9 +2933,9 @@ snapshots: glob: 10.5.0 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 + istanbul-lib-instrument: 6.0.3(supports-color@8.1.1) istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 + istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) istanbul-reports: 3.2.0 jest-message-util: 30.4.1 jest-util: 30.4.1 @@ -2974,12 +2977,12 @@ snapshots: jest-haste-map: 30.4.1 slash: 3.0.0 - '@jest/transform@30.4.1': + '@jest/transform@30.4.1(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@jest/types': 30.4.1 '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 7.0.1 + babel-plugin-istanbul: 7.0.1(supports-color@8.1.1) chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 @@ -3137,9 +3140,9 @@ snapshots: tslib: 2.8.1 optional: true - '@types/axios@0.14.4': + '@types/axios@0.14.4(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - axios: 1.19.0 + axios: 1.19.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - debug - supports-color @@ -3178,10 +3181,6 @@ snapshots: '@types/node': 22.20.1 '@types/ssh2': 1.15.5 - '@types/dotenv@8.2.3': - dependencies: - dotenv: 16.6.1 - '@types/estree@1.0.9': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -3223,15 +3222,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.67.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/parser': 8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/type-utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3) - '@typescript-eslint/utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.67.0 - eslint: 9.39.5 + eslint: 9.39.5(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -3239,23 +3238,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/parser@8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.67.0 '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.67.0 - debug: 4.4.3 - eslint: 9.39.5 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.5(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.67.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.67.0(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) '@typescript-eslint/types': 8.67.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3269,13 +3268,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.67.0(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3) - debug: 4.4.3 - eslint: 9.39.5 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.5(supports-color@8.1.1) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -3283,13 +3282,13 @@ snapshots: '@typescript-eslint/types@8.67.0': {} - '@typescript-eslint/typescript-estree@8.67.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.67.0(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.67.0(typescript@5.9.3) + '@typescript-eslint/project-service': 8.67.0(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) '@typescript-eslint/types': 8.67.0 '@typescript-eslint/visitor-keys': 8.67.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 @@ -3298,13 +3297,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.67.0(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/utils@8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@8.1.1)) '@typescript-eslint/scope-manager': 8.67.0 '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3) - eslint: 9.39.5 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@8.1.1)(typescript@5.9.3) + eslint: 9.39.5(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3314,10 +3313,10 @@ snapshots: '@typescript-eslint/types': 8.67.0 eslint-visitor-keys: 5.0.1 - '@typespec/ts-http-runtime@0.3.8': + '@typespec/ts-http-runtime@0.3.8(supports-color@8.1.1)': dependencies: - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -3406,9 +3405,9 @@ snapshots: acorn@8.18.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -3460,35 +3459,35 @@ snapshots: asynckit@0.4.0: {} - axios@1.19.0: + axios@1.19.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) form-data: 4.0.6 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@8.1.1) proxy-from-env: 2.1.0 transitivePeerDependencies: - debug - supports-color - babel-jest@30.4.1(@babel/core@7.29.7): + babel-jest@30.4.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@jest/transform': 30.4.1 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@jest/transform': 30.4.1(supports-color@8.1.1) '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.4.0(@babel/core@7.29.7) + babel-plugin-istanbul: 7.0.1(supports-color@8.1.1) + babel-preset-jest: 30.4.0(@babel/core@7.29.7(supports-color@8.1.1)) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-plugin-istanbul@7.0.1: + babel-plugin-istanbul@7.0.1(supports-color@8.1.1): dependencies: '@babel/helper-plugin-utils': 7.29.7 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.6 - istanbul-lib-instrument: 6.0.3 + istanbul-lib-instrument: 6.0.3(supports-color@8.1.1) test-exclude: 6.0.0 transitivePeerDependencies: - supports-color @@ -3497,30 +3496,30 @@ snapshots: dependencies: '@types/babel__core': 7.20.5 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): - dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) - '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) - - babel-preset-jest@30.4.0(@babel/core@7.29.7): - dependencies: - '@babel/core': 7.29.7 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7(supports-color@8.1.1)): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7(supports-color@8.1.1)) + + babel-preset-jest@30.4.0(@babel/core@7.29.7(supports-color@8.1.1)): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) babel-plugin-jest-hoist: 30.4.0 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7(supports-color@8.1.1)) balanced-match@1.0.2: {} @@ -3664,9 +3663,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.3: + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 dedent@1.7.2: {} @@ -3680,29 +3681,27 @@ snapshots: detect-newline@3.1.0: {} - docker-modem@5.0.7: + docker-modem@5.0.7(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.17.0 transitivePeerDependencies: - supports-color - dockerode@4.0.12: + dockerode@4.0.12(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@grpc/grpc-js': 1.14.4 '@grpc/proto-loader': 0.7.15 - docker-modem: 5.0.7 + docker-modem: 5.0.7(supports-color@8.1.1) protobufjs: 7.6.5 tar-fs: 2.1.5 uuid: 11.1.1 transitivePeerDependencies: - supports-color - dotenv@16.6.1: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3761,14 +3760,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.5: + eslint@9.39.5(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 + '@eslint/config-array': 0.21.2(supports-color@8.1.1) '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.6 + '@eslint/eslintrc': 3.3.6(supports-color@8.1.1) '@eslint/js': 9.39.5 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 @@ -3778,7 +3777,7 @@ snapshots: ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -3896,7 +3895,9 @@ snapshots: flatted@3.4.4: {} - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@8.1.1)): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) foreground-child@3.3.1: dependencies: @@ -4008,24 +4009,24 @@ snapshots: html-escaper@2.0.2: {} - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@8.1.1): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@8.1.1): dependencies: - agent-base: 6.0.2 - debug: 4.4.3 + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@8.1.1): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -4076,9 +4077,9 @@ snapshots: istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@6.0.3: + istanbul-lib-instrument@6.0.3(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/parser': 7.29.8 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 @@ -4092,10 +4093,10 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: + istanbul-lib-source-maps@5.0.6(supports-color@8.1.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -4117,10 +4118,10 @@ snapshots: jest-util: 30.4.1 p-limit: 3.1.0 - jest-circus@30.4.2: + jest-circus@30.4.2(supports-color@8.1.1): dependencies: '@jest/environment': 30.4.1 - '@jest/expect': 30.4.1 + '@jest/expect': 30.4.1(supports-color@8.1.1) '@jest/test-result': 30.4.1 '@jest/types': 30.4.1 '@types/node': 22.20.1 @@ -4131,8 +4132,8 @@ snapshots: jest-each: 30.4.1 jest-matcher-utils: 30.4.1 jest-message-util: 30.4.1 - jest-runtime: 30.4.2 - jest-snapshot: 30.4.1 + jest-runtime: 30.4.2(supports-color@8.1.1) + jest-snapshot: 30.4.1(supports-color@8.1.1) jest-util: 30.4.1 p-limit: 3.1.0 pretty-format: 30.4.1 @@ -4143,15 +4144,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.4.2(@types/node@22.20.1): + jest-cli@30.4.2(@types/node@22.20.1)(supports-color@8.1.1): dependencies: - '@jest/core': 30.4.2 + '@jest/core': 30.4.2(supports-color@8.1.1) '@jest/test-result': 30.4.1 '@jest/types': 30.4.1 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.4.2(@types/node@22.20.1) + jest-config: 30.4.2(@types/node@22.20.1)(supports-color@8.1.1) jest-util: 30.4.1 jest-validate: 30.4.1 yargs: 17.7.3 @@ -4162,25 +4163,25 @@ snapshots: - supports-color - ts-node - jest-config@30.4.2(@types/node@22.20.1): + jest-config@30.4.2(@types/node@22.20.1)(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@jest/get-type': 30.1.0 '@jest/pattern': 30.4.0 '@jest/test-sequencer': 30.4.1 '@jest/types': 30.4.1 - babel-jest: 30.4.1(@babel/core@7.29.7) + babel-jest: 30.4.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) chalk: 4.1.2 ci-info: 4.4.0 deepmerge: 4.3.1 glob: 10.5.0 graceful-fs: 4.2.11 - jest-circus: 30.4.2 + jest-circus: 30.4.2(supports-color@8.1.1) jest-docblock: 30.4.0 jest-environment-node: 30.4.1 jest-regex-util: 30.4.0 jest-resolve: 30.4.1 - jest-runner: 30.4.2 + jest-runner: 30.4.2(supports-color@8.1.1) jest-util: 30.4.1 jest-validate: 30.4.1 parse-json: 5.2.0 @@ -4274,10 +4275,10 @@ snapshots: jest-regex-util@30.4.0: {} - jest-resolve-dependencies@30.4.2: + jest-resolve-dependencies@30.4.2(supports-color@8.1.1): dependencies: jest-regex-util: 30.4.0 - jest-snapshot: 30.4.1 + jest-snapshot: 30.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -4292,12 +4293,12 @@ snapshots: slash: 3.0.0 unrs-resolver: 1.12.2 - jest-runner@30.4.2: + jest-runner@30.4.2(supports-color@8.1.1): dependencies: '@jest/console': 30.4.1 '@jest/environment': 30.4.1 '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 + '@jest/transform': 30.4.1(supports-color@8.1.1) '@jest/types': 30.4.1 '@types/node': 22.20.1 chalk: 4.1.2 @@ -4310,7 +4311,7 @@ snapshots: jest-leak-detector: 30.4.1 jest-message-util: 30.4.1 jest-resolve: 30.4.1 - jest-runtime: 30.4.2 + jest-runtime: 30.4.2(supports-color@8.1.1) jest-util: 30.4.1 jest-watcher: 30.4.1 jest-worker: 30.4.1 @@ -4319,14 +4320,14 @@ snapshots: transitivePeerDependencies: - supports-color - jest-runtime@30.4.2: + jest-runtime@30.4.2(supports-color@8.1.1): dependencies: '@jest/environment': 30.4.1 '@jest/fake-timers': 30.4.1 - '@jest/globals': 30.4.1 + '@jest/globals': 30.4.1(supports-color@8.1.1) '@jest/source-map': 30.0.1 '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 + '@jest/transform': 30.4.1(supports-color@8.1.1) '@jest/types': 30.4.1 '@types/node': 22.20.1 chalk: 4.1.2 @@ -4339,26 +4340,26 @@ snapshots: jest-mock: 30.4.1 jest-regex-util: 30.4.0 jest-resolve: 30.4.1 - jest-snapshot: 30.4.1 + jest-snapshot: 30.4.1(supports-color@8.1.1) jest-util: 30.4.1 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@30.4.1: + jest-snapshot@30.4.1(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.8 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/types': 7.29.8 '@jest/expect-utils': 30.4.1 '@jest/get-type': 30.1.0 '@jest/snapshot-utils': 30.4.1 - '@jest/transform': 30.4.1 + '@jest/transform': 30.4.1(supports-color@8.1.1) '@jest/types': 30.4.1 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7(supports-color@8.1.1)) chalk: 4.1.2 expect: 30.4.1 graceful-fs: 4.2.11 @@ -4409,12 +4410,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.4.2(@types/node@22.20.1): + jest@30.4.2(@types/node@22.20.1)(supports-color@8.1.1): dependencies: - '@jest/core': 30.4.2 + '@jest/core': 30.4.2(supports-color@8.1.1) '@jest/types': 30.4.1 import-local: 3.2.0 - jest-cli: 30.4.2(@types/node@22.20.1) + jest-cli: 30.4.2(@types/node@22.20.1)(supports-color@8.1.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -4810,12 +4811,12 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@22.20.1))(typescript@5.9.3): + ts-jest@29.4.12(@babel/core@7.29.7(supports-color@8.1.1))(@jest/transform@30.4.1(supports-color@8.1.1))(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(jest-util@30.4.1)(jest@30.4.2(@types/node@22.20.1)(supports-color@8.1.1))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 30.4.2(@types/node@22.20.1) + jest: 30.4.2(@types/node@22.20.1)(supports-color@8.1.1) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -4824,10 +4825,10 @@ snapshots: typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.29.7 - '@jest/transform': 30.4.1 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@jest/transform': 30.4.1(supports-color@8.1.1) '@jest/types': 30.4.1 - babel-jest: 30.4.1(@babel/core@7.29.7) + babel-jest: 30.4.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) jest-util: 30.4.1 tslib@1.14.1: {} @@ -4838,6 +4839,8 @@ snapshots: tweetnacl@0.14.5: {} + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -4848,13 +4851,13 @@ snapshots: type-fest@4.41.0: {} - typescript-eslint@8.67.0(eslint@9.39.5)(typescript@5.9.3): + typescript-eslint@8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) - '@typescript-eslint/parser': 8.67.0(eslint@9.39.5)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3) - eslint: 9.39.5 + '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.67.0(eslint@9.39.5(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + eslint: 9.39.5(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color diff --git a/setup/.env b/setup/.env deleted file mode 100644 index cf1aca70..00000000 --- a/setup/.env +++ /dev/null @@ -1 +0,0 @@ -PERSONAL_ACCESS_TOKEN= diff --git a/setup/workflows/agent-cli-provisioning.yml b/setup/workflows/agent-cli-provisioning.yml index 39fc38ca..a4537024 100644 --- a/setup/workflows/agent-cli-provisioning.yml +++ b/setup/workflows/agent-cli-provisioning.yml @@ -19,36 +19,34 @@ jobs: shell: bash env: AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} + FINDINGS_PROVIDER: ${{ vars.FINDINGS_PROVIDER }} + FIXER_PROVIDER: ${{ vars.FIXER_PROVIDER }} + PLANNER_PROVIDER: ${{ vars.PLANNER_PROVIDER }} + REVIEWER_PROVIDER: ${{ vars.REVIEWER_PROVIDER }} + TESTER_PROVIDER: ${{ vars.TESTER_PROVIDER }} + RELEASE_PROVIDER: ${{ vars.RELEASE_PROVIDER }} AGENT_COMMAND: ${{ vars.AGENT_COMMAND }} run: | set -euo pipefail - if [[ -n "${AGENT_COMMAND// }" ]]; then - read -r executable _ <<< "$AGENT_COMMAND" - command -v "$executable" - echo "Configured agent command: $executable" - else - case "$AGENT_PROVIDER" in - opencode) - command -v opencode - opencode --version >/dev/null - opencode run --help >/dev/null - ;; - codex) - command -v codex - codex --version >/dev/null - codex exec --help >/dev/null - ;; - cursor) - command -v agent - agent --version >/dev/null - agent --help >/dev/null - ;; - *) - echo "Unsupported AGENT_PROVIDER: $AGENT_PROVIDER" >&2 - exit 1 - ;; + providers=("$AGENT_PROVIDER" "$FINDINGS_PROVIDER" "$FIXER_PROVIDER" "$PLANNER_PROVIDER" "$REVIEWER_PROVIDER" "$TESTER_PROVIDER" "$RELEASE_PROVIDER") + checked="" + for provider in "${providers[@]}"; do + [[ -z "${provider// }" ]] && continue + [[ " $checked " == *" $provider "* ]] && continue + checked="$checked $provider" + if [[ -n "${AGENT_COMMAND// }" && "$provider" == "$AGENT_PROVIDER" ]]; then + read -r executable _ <<< "$AGENT_COMMAND" + command -v "$executable" + echo "Configured agent command: $executable" + continue + fi + case "$provider" in + opencode) command -v opencode; opencode --version >/dev/null; opencode run --help >/dev/null ;; + codex) command -v codex; codex --version >/dev/null; codex exec --help >/dev/null ;; + cursor) command -v agent; agent --version >/dev/null; agent --help >/dev/null ;; + *) echo "Unsupported agent provider: $provider" >&2; exit 1 ;; esac - fi + done - echo "Agent CLI contract passed for $AGENT_PROVIDER." + echo "Agent CLI contract passed for: $checked." diff --git a/setup/workflows/copilot_commit.yml b/setup/workflows/copilot_commit.yml index 94b8bc98..21e3df14 100644 --- a/setup/workflows/copilot_commit.yml +++ b/setup/workflows/copilot_commit.yml @@ -18,7 +18,34 @@ jobs: - uses: vypdev/copilot@v3 with: + ai-ignore-files: ${{ vars.AI_IGNORE_FILES || 'build/*' }} debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + feature-tree: ${{ vars.FEATURE_TREE || 'feature' }} + bugfix-tree: ${{ vars.BUGFIX_TREE || 'bugfix' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + docs-tree: ${{ vars.DOCS_TREE || 'docs' }} + chore-tree: ${{ vars.CHORE_TREE || 'chore' }} + branch-management-always: ${{ vars.BRANCH_MANAGEMENT_ALWAYS || 'false' }} + reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} + desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} + desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} + merge-timeout: ${{ vars.MERGE_TIMEOUT || '600' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} + ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} + ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} + bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} + bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} + bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} + project-column-issue-created: ${{ vars.PROJECT_COLUMN_ISSUE_CREATED || 'Todo' }} + project-column-pull-request-created: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_CREATED || 'In Progress' }} + project-column-issue-in-progress: ${{ vars.PROJECT_COLUMN_ISSUE_IN_PROGRESS || 'In Progress' }} + project-column-pull-request-in-progress: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS || 'In Progress' }} project-ids: ${{ vars.PROJECT_IDS }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} diff --git a/setup/workflows/copilot_credential_health.yml b/setup/workflows/copilot_credential_health.yml new file mode 100644 index 00000000..3416e2dd --- /dev/null +++ b/setup/workflows/copilot_credential_health.yml @@ -0,0 +1,170 @@ +name: Copilot - Credential Health + +on: + workflow_dispatch: + inputs: + check_pat: + description: Check the workflow PAT against GitHub + required: false + default: false + type: boolean + check_openai: + description: Check OPENAI_API_KEY + required: false + default: false + type: boolean + check_anthropic: + description: Check ANTHROPIC_API_KEY + required: false + default: false + type: boolean + check_google: + description: Check GOOGLE_API_KEY + required: false + default: false + type: boolean + check_openrouter: + description: Check OPENROUTER_API_KEY + required: false + default: false + type: boolean + check_cursor: + description: Check CURSOR_API_KEY + required: false + default: false + type: boolean + check_opencode: + description: Check OPENCODE_API_KEY + required: false + default: false + type: boolean + check_codex: + description: Check CODEX_ACCESS_TOKEN + required: false + default: false + type: boolean + +permissions: + contents: read + +jobs: + verify-pat: + if: ${{ inputs.check_pat }} + name: Verify PAT + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate PAT without exposing its value + shell: bash + env: + PAT: ${{ secrets.PAT }} + run: | + set -euo pipefail + [[ -n "${PAT:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $PAT" -H 'Accept: application/vnd.github+json' https://api.github.com/user >/dev/null + curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $PAT" -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/${GITHUB_REPOSITORY}" >/dev/null + + verify-openai: + if: ${{ inputs.check_openai }} + name: Verify OPENAI_API_KEY + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate OpenAI key without exposing its value + shell: bash + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + set -euo pipefail + [[ -n "${OPENAI_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENAI_API_KEY" https://api.openai.com/v1/models >/dev/null + + verify-anthropic: + if: ${{ inputs.check_anthropic }} + name: Verify ANTHROPIC_API_KEY + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate Anthropic key without exposing its value + shell: bash + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + set -euo pipefail + [[ -n "${ANTHROPIC_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' https://api.anthropic.com/v1/models >/dev/null + + verify-google: + if: ${{ inputs.check_google }} + name: Verify GOOGLE_API_KEY + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate Google key without exposing its value + shell: bash + env: + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + run: | + set -euo pipefail + [[ -n "${GOOGLE_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 "https://generativelanguage.googleapis.com/v1beta/models?key=$GOOGLE_API_KEY" >/dev/null + + verify-openrouter: + if: ${{ inputs.check_openrouter }} + name: Verify OPENROUTER_API_KEY + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate OpenRouter key without exposing its value + shell: bash + env: + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + run: | + set -euo pipefail + [[ -n "${OPENROUTER_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENROUTER_API_KEY" https://openrouter.ai/api/v1/models >/dev/null + + verify-cursor: + if: ${{ inputs.check_cursor }} + name: Verify CURSOR_API_KEY + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate Cursor key without exposing its value + shell: bash + env: + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + run: | + set -euo pipefail + [[ -n "${CURSOR_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 -u "$CURSOR_API_KEY:" 'https://api.cursor.com/analytics/ai-code/changes?startDate=30d&page=1&pageSize=1' >/dev/null + + verify-opencode: + if: ${{ inputs.check_opencode }} + name: Verify OPENCODE_API_KEY + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate OpenCode key without exposing its value + shell: bash + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + run: | + set -euo pipefail + [[ -n "${OPENCODE_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENCODE_API_KEY" https://opencode.ai/zen/v1/models >/dev/null + + verify-codex: + if: ${{ inputs.check_codex }} + name: Verify CODEX_ACCESS_TOKEN + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate Codex token without exposing its value + shell: bash + env: + CODEX_ACCESS_TOKEN: ${{ secrets.CODEX_ACCESS_TOKEN }} + run: | + set -euo pipefail + [[ -n "${CODEX_ACCESS_TOKEN:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; } + curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $CODEX_ACCESS_TOKEN" https://api.openai.com/v1/models >/dev/null diff --git a/setup/workflows/copilot_issue.yml b/setup/workflows/copilot_issue.yml index 522eca2a..871a3623 100644 --- a/setup/workflows/copilot_issue.yml +++ b/setup/workflows/copilot_issue.yml @@ -17,8 +17,35 @@ jobs: - uses: vypdev/copilot@v3 with: - ai-ignore-files: build/* + ai-ignore-files: ${{ vars.AI_IGNORE_FILES || 'build/*' }} debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + feature-tree: ${{ vars.FEATURE_TREE || 'feature' }} + bugfix-tree: ${{ vars.BUGFIX_TREE || 'bugfix' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + docs-tree: ${{ vars.DOCS_TREE || 'docs' }} + chore-tree: ${{ vars.CHORE_TREE || 'chore' }} + branch-management-always: ${{ vars.BRANCH_MANAGEMENT_ALWAYS || 'false' }} + reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} + desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} + desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} + merge-timeout: ${{ vars.MERGE_TIMEOUT || '600' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} + ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} + ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} + bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} + bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} + bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} + project-ids: ${{ vars.PROJECT_IDS }} + project-column-issue-created: ${{ vars.PROJECT_COLUMN_ISSUE_CREATED || 'Todo' }} + project-column-pull-request-created: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_CREATED || 'In Progress' }} + project-column-issue-in-progress: ${{ vars.PROJECT_COLUMN_ISSUE_IN_PROGRESS || 'In Progress' }} + project-column-pull-request-in-progress: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS || 'In Progress' }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} diff --git a/setup/workflows/copilot_issue_comment.yml b/setup/workflows/copilot_issue_comment.yml index 2e898906..7f82eccc 100644 --- a/setup/workflows/copilot_issue_comment.yml +++ b/setup/workflows/copilot_issue_comment.yml @@ -17,8 +17,34 @@ jobs: - uses: vypdev/copilot@v3 with: - ai-ignore-files: build/* + ai-ignore-files: ${{ vars.AI_IGNORE_FILES || 'build/*' }} debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + feature-tree: ${{ vars.FEATURE_TREE || 'feature' }} + bugfix-tree: ${{ vars.BUGFIX_TREE || 'bugfix' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + docs-tree: ${{ vars.DOCS_TREE || 'docs' }} + chore-tree: ${{ vars.CHORE_TREE || 'chore' }} + branch-management-always: ${{ vars.BRANCH_MANAGEMENT_ALWAYS || 'false' }} + reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} + desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} + desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} + merge-timeout: ${{ vars.MERGE_TIMEOUT || '600' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} + ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} + ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} + bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} + bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} + bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} + project-column-issue-created: ${{ vars.PROJECT_COLUMN_ISSUE_CREATED || 'Todo' }} + project-column-pull-request-created: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_CREATED || 'In Progress' }} + project-column-issue-in-progress: ${{ vars.PROJECT_COLUMN_ISSUE_IN_PROGRESS || 'In Progress' }} + project-column-pull-request-in-progress: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS || 'In Progress' }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} @@ -40,7 +66,6 @@ jobs: planner-effort: ${{ vars.PLANNER_EFFORT }} planner-command: ${{ vars.PLANNER_COMMAND }} project-ids: ${{ vars.PROJECT_IDS }} - bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} token: ${{ secrets.PAT }} env: AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} diff --git a/setup/workflows/copilot_pull_request.yml b/setup/workflows/copilot_pull_request.yml index 3ca5a52b..f93aa0ae 100644 --- a/setup/workflows/copilot_pull_request.yml +++ b/setup/workflows/copilot_pull_request.yml @@ -15,8 +15,34 @@ jobs: - uses: vypdev/copilot@v3 with: - ai-ignore-files: build/* + ai-ignore-files: ${{ vars.AI_IGNORE_FILES || 'build/*' }} debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + feature-tree: ${{ vars.FEATURE_TREE || 'feature' }} + bugfix-tree: ${{ vars.BUGFIX_TREE || 'bugfix' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + docs-tree: ${{ vars.DOCS_TREE || 'docs' }} + chore-tree: ${{ vars.CHORE_TREE || 'chore' }} + branch-management-always: ${{ vars.BRANCH_MANAGEMENT_ALWAYS || 'false' }} + reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} + desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} + desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} + merge-timeout: ${{ vars.MERGE_TIMEOUT || '600' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} + ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} + ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} + bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} + bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} + bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} + project-column-issue-created: ${{ vars.PROJECT_COLUMN_ISSUE_CREATED || 'Todo' }} + project-column-pull-request-created: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_CREATED || 'In Progress' }} + project-column-issue-in-progress: ${{ vars.PROJECT_COLUMN_ISSUE_IN_PROGRESS || 'In Progress' }} + project-column-pull-request-in-progress: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS || 'In Progress' }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} diff --git a/setup/workflows/copilot_pull_request_comment.yml b/setup/workflows/copilot_pull_request_comment.yml index 34393ff6..d8d8d02b 100644 --- a/setup/workflows/copilot_pull_request_comment.yml +++ b/setup/workflows/copilot_pull_request_comment.yml @@ -17,8 +17,34 @@ jobs: - uses: vypdev/copilot@v3 with: - ai-ignore-files: build/* + ai-ignore-files: ${{ vars.AI_IGNORE_FILES || 'build/*' }} debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + feature-tree: ${{ vars.FEATURE_TREE || 'feature' }} + bugfix-tree: ${{ vars.BUGFIX_TREE || 'bugfix' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + docs-tree: ${{ vars.DOCS_TREE || 'docs' }} + chore-tree: ${{ vars.CHORE_TREE || 'chore' }} + branch-management-always: ${{ vars.BRANCH_MANAGEMENT_ALWAYS || 'false' }} + reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} + desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} + desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} + merge-timeout: ${{ vars.MERGE_TIMEOUT || '600' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} + ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} + ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} + bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} + bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} + bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} + project-column-issue-created: ${{ vars.PROJECT_COLUMN_ISSUE_CREATED || 'Todo' }} + project-column-pull-request-created: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_CREATED || 'In Progress' }} + project-column-issue-in-progress: ${{ vars.PROJECT_COLUMN_ISSUE_IN_PROGRESS || 'In Progress' }} + project-column-pull-request-in-progress: ${{ vars.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS || 'In Progress' }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} @@ -40,7 +66,6 @@ jobs: reviewer-effort: ${{ vars.REVIEWER_EFFORT }} reviewer-command: ${{ vars.REVIEWER_COMMAND }} project-ids: ${{ vars.PROJECT_IDS }} - bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} token: ${{ secrets.PAT }} env: AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} diff --git a/setup/workflows/hotfix_workflow.yml b/setup/workflows/hotfix_workflow.yml index ab2415b7..5db5d63d 100644 --- a/setup/workflows/hotfix_workflow.yml +++ b/setup/workflows/hotfix_workflow.yml @@ -136,6 +136,10 @@ jobs: if: ${{ success() }} with: debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} single-action: 'create_tag' single-action-issue: '${{ github.event.inputs.issue }}' single-action-version: '${{ github.event.inputs.version }}' @@ -161,6 +165,10 @@ jobs: if: ${{ success() }} with: debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} single-action: 'create_release' single-action-issue: '${{ github.event.inputs.issue }}' single-action-version: '${{ github.event.inputs.version }}' @@ -192,6 +200,10 @@ jobs: if: ${{ success() }} with: debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} single-action: 'deployed_action' single-action-issue: '${{ github.event.inputs.issue }}' agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} diff --git a/setup/workflows/release_workflow.yml b/setup/workflows/release_workflow.yml index 048cd8cf..e14a0966 100644 --- a/setup/workflows/release_workflow.yml +++ b/setup/workflows/release_workflow.yml @@ -136,6 +136,10 @@ jobs: if: ${{ success() }} with: debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} single-action: 'create_tag' single-action-issue: '${{ github.event.inputs.issue }}' single-action-version: '${{ github.event.inputs.version }}' @@ -161,6 +165,10 @@ jobs: if: ${{ success() }} with: debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} single-action: 'create_release' single-action-issue: '${{ github.event.inputs.issue }}' single-action-version: '${{ github.event.inputs.version }}' @@ -192,6 +200,10 @@ jobs: if: ${{ success() }} with: debug: ${{ vars.DEBUG }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} single-action: 'deployed_action' single-action-issue: '${{ github.event.inputs.issue }}' agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 94e16b00..5f790611 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -44,6 +44,10 @@ jest.mock('../utils/setup_files', () => { }; }); +jest.mock('../infrastructure/composition/setup_credentials_composition_root', () => ({ + createSetupCredentialsUseCase: () => ({ collect: jest.fn().mockResolvedValue({ collection: { apiKeys: [] }, checks: [], existingSecretNames: [] }) }), +})); + describe('CLI', () => { let exitSpy: jest.SpyInstance; let consoleErrorSpy: jest.SpyInstance; @@ -312,6 +316,7 @@ describe('CLI', () => { 'setup', '--token', 'ghp_setup_test_token_xxxxxxxxxxxxxxxxxxxx', + '--skip-secrets', ]); expect(runLocalAction).toHaveBeenCalledTimes(1); @@ -322,7 +327,7 @@ describe('CLI', () => { }); it('proceeds when --token is provided even if env/.env has no token', async () => { - await program.parseAsync(['node', 'cli', 'setup', '--token', 'ghp_abcdefghijklmnopqrstuvwxyz12']); + await program.parseAsync(['node', 'cli', 'setup', '--token', 'ghp_abcdefghijklmnopqrstuvwxyz12', '--skip-secrets']); expect(exitSpy).not.toHaveBeenCalled(); expect(runLocalAction).toHaveBeenCalledTimes(1); @@ -362,30 +367,28 @@ describe('CLI', () => { expect(ranWithValidRepo).not.toBe(true); }); - it('exits when no valid token and suggests creating .env when .env does not exist', async () => { + it('exits when no valid setup token is available', async () => { mockGetSetupToken.mockReturnValue(undefined); - mockSetupEnvFileExists.mockReturnValue(false); const { logError, logInfo } = require('../utils/logger'); (runLocalAction as jest.Mock).mockClear(); await program.parseAsync(['node', 'cli', 'setup']); expect(logError).toHaveBeenCalledWith(expect.stringContaining('Setup requires PERSONAL_ACCESS_TOKEN')); - expect(logInfo).toHaveBeenCalledWith(expect.stringContaining('create a .env file')); + expect(logInfo).toHaveBeenCalledWith(expect.stringContaining('PERSONAL_ACCESS_TOKEN')); expect(runLocalAction).not.toHaveBeenCalled(); expect(exitSpy).toHaveBeenCalledWith(1); }); - it('exits when no valid token and suggests adding to existing .env when .env exists', async () => { + it('does not offer local .env configuration when the setup token is missing', async () => { mockGetSetupToken.mockReturnValue(undefined); - mockSetupEnvFileExists.mockReturnValue(true); const { logError, logInfo } = require('../utils/logger'); (runLocalAction as jest.Mock).mockClear(); await program.parseAsync(['node', 'cli', 'setup']); expect(logError).toHaveBeenCalledWith(expect.stringContaining('Setup requires PERSONAL_ACCESS_TOKEN')); - expect(logInfo).toHaveBeenCalledWith(expect.stringContaining('existing .env file')); + expect(logInfo).not.toHaveBeenCalledWith(expect.stringContaining('.env')); expect(runLocalAction).not.toHaveBeenCalled(); expect(exitSpy).toHaveBeenCalledWith(1); }); diff --git a/src/application/policies/__tests__/setup_configuration_policy.test.ts b/src/application/policies/__tests__/setup_configuration_policy.test.ts new file mode 100644 index 00000000..e644c170 --- /dev/null +++ b/src/application/policies/__tests__/setup_configuration_policy.test.ts @@ -0,0 +1,107 @@ +import { + buildSetupActionInputs, + buildSetupCredentialRequirements, + buildSetupPlan, + buildSetupRepositoryVariables, + createDefaultSetupConfiguration, + mergeSetupConfiguration, + validateSetupConfiguration, +} from '../setup_configuration_policy'; +import type { SetupConfigurationOverrides } from '../setup_configuration_policy'; + +describe('setup configuration policy', () => { + it('builds a complete safe default plan', () => { + const configuration = createDefaultSetupConfiguration(); + const plan = buildSetupPlan(configuration); + + expect(plan.workflowFiles).toHaveLength(9); + expect(plan.issueTemplateFiles).toHaveLength(8); + expect(plan.selectedFiles).toHaveLength(18); + expect(plan.variables).toEqual(expect.arrayContaining([ + { name: 'AGENT_PROVIDER', value: 'codex' }, + { name: 'AGENT_ALLOWED_MODELS', value: 'openai/gpt-5.6-luna' }, + { name: 'MAIN_BRANCH', value: 'master' }, + { name: 'AI_IGNORE_FILES', value: 'build/*' }, + ])); + expect(plan.requiredSecrets).toEqual(expect.arrayContaining(['PAT', 'CODEX_ACCESS_TOKEN', 'OPENAI_API_KEY'])); + expect(plan.warnings.length).toBeGreaterThan(0); + }); + + it('removes optional files while retaining core setup resources', () => { + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + features: { + release: false, + hotfix: false, + agentProvisioning: false, + issueTemplates: false, + pullRequestTemplate: false, + }, + }); + const plan = buildSetupPlan(configuration); + + expect(plan.workflowFiles).toEqual(expect.arrayContaining([ + 'copilot_issue.yml', + 'copilot_pull_request.yml', + 'copilot_commit.yml', + ])); + expect(plan.workflowFiles).not.toContain('release_workflow.yml'); + expect(plan.selectedFiles).toHaveLength(6); + }); + + it('supports independent agent runtime and model settings for every task', () => { + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + agents: { + planner: { provider: 'cursor', modelProvider: 'openai', model: 'gpt-5.6-luna' }, + reviewer: { provider: 'opencode', modelProvider: 'anthropic', model: 'claude-3-7-sonnet', effort: 'high' }, + }, + }); + const variables = buildSetupRepositoryVariables(configuration); + + expect(variables).toEqual(expect.arrayContaining([ + { name: 'PLANNER_PROVIDER', value: 'cursor' }, + { name: 'REVIEWER_PROVIDER', value: 'opencode' }, + { name: 'REVIEWER_MODEL_PROVIDER', value: 'anthropic' }, + { name: 'AGENT_ALLOWED_MODEL_PROVIDERS', value: 'openai,anthropic' }, + ])); + expect(buildSetupActionInputs(configuration)).toMatchObject({ + 'planner-provider': 'cursor', + 'reviewer-model': 'claude-3-7-sonnet', + }); + expect(buildSetupPlan(configuration).warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Cursor is an experimental runtime'), + ])); + }); + + it('derives runtime credentials without asking Cursor for an unused model-provider key', () => { + const opencode = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + agents: Object.fromEntries(['planner', 'findings', 'reviewer', 'fixer', 'tester', 'release'].map(task => [task, { + provider: 'opencode', modelProvider: 'openai', model: 'gpt-5.6-luna', + }])) as SetupConfigurationOverrides['agents'], + }); + expect(buildSetupCredentialRequirements(opencode).map(requirement => requirement.name)).toEqual([ + 'PAT', 'OPENCODE_API_KEY', 'OPENAI_API_KEY', + ]); + + const cursor = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + agents: Object.fromEntries(['planner', 'findings', 'reviewer', 'fixer', 'tester', 'release'].map(task => [task, { + provider: 'cursor', modelProvider: 'openai', model: 'composer-1', + }])) as SetupConfigurationOverrides['agents'], + }); + expect(buildSetupCredentialRequirements(cursor).map(requirement => requirement.name)).toEqual(['PAT', 'CURSOR_API_KEY']); + }); + + it('rejects invalid operational and agent values', () => { + const configuration = createDefaultSetupConfiguration(); + configuration.repository.desiredReviewersCount = 16; + configuration.repository.mainBranch = 'main branch'; + configuration.ai.bugbotCommentLimit = 0; + configuration.agents.planner.model = 'unsafe model'; + + expect(validateSetupConfiguration(configuration)).toEqual(expect.arrayContaining([ + 'Desired reviewers must be between 0 and 15.', + 'The main branch must be non-empty and contain no whitespace.', + 'Bugbot comment limit must be between 1 and 100.', + 'Model provider and model for planner cannot contain whitespace.', + ])); + }); +}); diff --git a/src/application/policies/setup_configuration_policy.ts b/src/application/policies/setup_configuration_policy.ts new file mode 100644 index 00000000..30fba3cd --- /dev/null +++ b/src/application/policies/setup_configuration_policy.ts @@ -0,0 +1,390 @@ +import type { AgentTask } from '../../domain/agent'; +import { + DEFAULT_AGENT_MODEL, + DEFAULT_AGENT_PROVIDER, + DEFAULT_MODEL_PROVIDER, +} from '../../domain/agent'; +import type { + SetupAgentConfiguration, + SetupAgentRoleConfiguration, + SetupConfiguration, + SetupFeatures, + SetupPlan, + SetupVariable, + SetupCredentialRequirement, +} from '../../domain/setup'; +import { SUPPORTED_AGENT_PROVIDERS } from './agent_configuration_validation_policy'; + +export const SETUP_AGENT_TASKS: readonly AgentTask[] = [ + 'planner', + 'findings', + 'reviewer', + 'fixer', + 'tester', + 'release', +]; + +export const SETUP_FEATURE_DESCRIPTIONS: Readonly> = { + issues: 'Issue automation: branching, labels, projects, and issue lifecycle', + pullRequests: 'Pull request automation: review, descriptions, and lifecycle', + commits: 'Commit automation: progress, sizing, and Bugbot analysis', + issueComments: 'Issue comments: questions, translations, and Bugbot autofix', + pullRequestComments: 'Pull request review comments: translations and Bugbot autofix', + release: 'Release workflow: versioning, changelog, tag, and GitHub Release', + hotfix: 'Hotfix workflow: emergency release from a production tag', + agentProvisioning: 'Agent CLI provisioning check workflow', + credentialHealth: 'Read-only remote credential health workflow for setup and doctor', + issueTemplates: 'Issue templates for feature, bug, documentation, and operations', + pullRequestTemplate: 'Pull request template', +}; + +const WORKFLOW_FILES: Readonly> = { + issues: ['copilot_issue.yml'], + pullRequests: ['copilot_pull_request.yml'], + commits: ['copilot_commit.yml'], + issueComments: ['copilot_issue_comment.yml'], + pullRequestComments: ['copilot_pull_request_comment.yml'], + release: ['release_workflow.yml'], + hotfix: ['hotfix_workflow.yml'], + agentProvisioning: ['agent-cli-provisioning.yml'], + credentialHealth: ['copilot_credential_health.yml'], +}; + +const ISSUE_TEMPLATE_FILES = [ + 'config.yml', + 'feature_request.yml', + 'bug_report.yml', + 'doc_update.yml', + 'chore_task.yml', + 'help_request.yml', + 'hotfix.yml', + 'release.yml', +]; + +const SECRET_BY_MODEL_PROVIDER: Readonly> = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + google: 'GOOGLE_API_KEY', + openrouter: 'OPENROUTER_API_KEY', +}; + +export function createDefaultSetupConfiguration(): SetupConfiguration { + const defaultRole = (): SetupAgentRoleConfiguration => ({ + provider: DEFAULT_AGENT_PROVIDER, + modelProvider: DEFAULT_MODEL_PROVIDER, + model: DEFAULT_AGENT_MODEL, + effort: '', + }); + const agents = Object.fromEntries( + SETUP_AGENT_TASKS.map(task => [task, defaultRole()]), + ) as SetupAgentConfiguration; + const features: SetupFeatures = Object.fromEntries( + Object.keys(SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true]), + ); + return { + features, + agents, + repository: { + mainBranch: 'master', + developmentBranch: 'develop', + featureTree: 'feature', + bugfixTree: 'bugfix', + hotfixTree: 'hotfix', + releaseTree: 'release', + docsTree: 'docs', + choreTree: 'chore', + branchManagementAlways: false, + reopenIssueOnPush: true, + desiredAssigneesCount: 1, + desiredReviewersCount: 1, + mergeTimeout: 600, + issueLocale: 'en-US', + pullRequestLocale: 'en-US', + commitPrefixTransforms: 'replace-slash', + }, + ai: { + pullRequestDescription: true, + ignoreFiles: 'build/*', + membersOnly: false, + includeReasoning: true, + bugbotSeverity: 'low', + bugbotCommentLimit: 20, + bugbotFixVerifyCommands: '', + provisioningMode: 'auto', + }, + projects: { + ids: '', + issueCreatedColumn: 'Todo', + pullRequestCreatedColumn: 'In Progress', + issueInProgressColumn: 'In Progress', + pullRequestInProgressColumn: 'In Progress', + }, + createInitialTag: true, + manageRepositoryVariables: true, + manageRepositorySecrets: true, + actionInputs: {}, + }; +} + +export type SetupConfigurationOverrides = { + features?: Partial; + agents?: Partial>>; + repository?: Partial; + ai?: Partial; + projects?: Partial; + createInitialTag?: boolean; + manageRepositoryVariables?: boolean; + manageRepositorySecrets?: boolean; + actionInputs?: Record; +}; + +export function mergeSetupConfiguration( + base: SetupConfiguration, + overrides: SetupConfigurationOverrides = {}, +): SetupConfiguration { + const agents = { ...base.agents } as SetupAgentConfiguration; + for (const task of SETUP_AGENT_TASKS) { + agents[task] = { ...base.agents[task], ...(overrides.agents?.[task] ?? {}) }; + } + return { + ...base, + features: { ...base.features, ...(overrides.features ?? {}) } as SetupFeatures, + agents, + repository: { ...base.repository, ...(overrides.repository ?? {}) }, + ai: { ...base.ai, ...(overrides.ai ?? {}) }, + projects: { ...base.projects, ...(overrides.projects ?? {}) }, + createInitialTag: overrides.createInitialTag ?? base.createInitialTag, + manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, + manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, + actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, + }; +} + +export function validateSetupConfiguration(configuration: SetupConfiguration): string[] { + const errors: string[] = []; + const nonEmpty = [ + ['main branch', configuration.repository.mainBranch], + ['development branch', configuration.repository.developmentBranch], + ['feature branch prefix', configuration.repository.featureTree], + ['bugfix branch prefix', configuration.repository.bugfixTree], + ['hotfix branch prefix', configuration.repository.hotfixTree], + ['release branch prefix', configuration.repository.releaseTree], + ['docs branch prefix', configuration.repository.docsTree], + ['chore branch prefix', configuration.repository.choreTree], + ] as const; + for (const [name, value] of nonEmpty) { + if (!value.trim() || /\s/.test(value)) errors.push(`The ${name} must be non-empty and contain no whitespace.`); + } + if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { + errors.push('Desired assignees must be between 0 and 10.'); + } + if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { + errors.push('Desired reviewers must be between 0 and 15.'); + } + if (configuration.repository.mergeTimeout < 0) errors.push('Merge timeout cannot be negative.'); + if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { + errors.push('Bugbot comment limit must be between 1 and 100.'); + } + if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { + errors.push('Bugbot severity must be info, low, medium, or high.'); + } + if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { + errors.push('Agent provisioning must be auto, always, or disabled.'); + } + for (const task of SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (!SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); + if (!agent.modelProvider.trim() || !agent.model.trim()) errors.push(`Model provider and model are required for ${task}.`); + if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) errors.push(`Model provider and model for ${task} cannot contain whitespace.`); + } + return errors; +} + +export function buildSetupPlan(configuration: SetupConfiguration): SetupPlan { + const workflowFiles = Object.entries(WORKFLOW_FILES) + .filter(([feature]) => configuration.features[feature] !== false) + .flatMap(([, files]) => files); + const issueTemplateFiles = configuration.features.issueTemplates === false + ? [] + : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') + .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); + const selectedFiles = [ + ...workflowFiles.map(file => `workflows/${file}`), + ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), + ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), + ]; + return { + configuration, + workflowFiles, + issueTemplateFiles, + selectedFiles, + variables: buildSetupRepositoryVariables(configuration), + requiredSecrets: buildRequiredSetupSecrets(configuration), + credentialRequirements: buildSetupCredentialRequirements(configuration), + warnings: buildSetupWarnings(configuration), + }; +} + +/** Builds the non-sensitive credential contract implied by the selected agents. */ +export function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[] { + const requirements = new Map(); + const add = (name: string, kind: SetupCredentialRequirement['kind'], description: string, provider?: string, model?: string) => { + if (!requirements.has(name)) requirements.set(name, { name, kind, description, provider, model }); + }; + add('PAT', 'workflowPat', 'A separate GitHub token owned by the bot account. It is used by workflows at runtime.'); + for (const task of SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (agent.provider === 'cursor') { + add('CURSOR_API_KEY', 'apiKey', 'Cursor API key used by the Cursor agent runtime.', 'cursor', agent.model); + continue; + } + if (agent.provider === 'opencode') add('OPENCODE_API_KEY', 'apiKey', 'OpenCode API key used by the OpenCode agent runtime.', 'opencode', agent.model); + if (agent.provider === 'codex') add('CODEX_ACCESS_TOKEN', 'apiKey', 'Codex access token used by the Codex agent runtime.', 'codex', agent.model); + const modelProvider = agent.modelProvider.trim().toLowerCase(); + if (modelProvider && !['local', 'ollama', 'lmstudio'].includes(modelProvider)) { + const name = SECRET_BY_MODEL_PROVIDER[modelProvider] ?? `${modelProvider.replace(/-/g, '_').toUpperCase()}_API_KEY`; + add(name, 'apiKey', `${modelProvider} API key for ${agent.model}.`, modelProvider, agent.model); + } + } + return [...requirements.values()]; +} + +export function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[] { + const variables: SetupVariable[] = []; + const add = (name: string, value: string | number | boolean | undefined) => { + if (value === undefined || value === '') return; + variables.push({ name, value: String(value) }); + }; + const base = configuration.agents.findings; + add('AGENT_PROVIDER', base.provider); + add('AGENT_MODEL_PROVIDER', base.modelProvider); + add('AGENT_MODEL', base.model); + add('AGENT_EFFORT', base.effort); + add('AGENT_PROVISIONING', configuration.ai.provisioningMode); + add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); + add('AGENT_ALLOWED_MODELS', unique(SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); + + for (const task of SETUP_AGENT_TASKS) { + const prefix = task.toUpperCase(); + const agent = configuration.agents[task]; + add(`${prefix}_PROVIDER`, agent.provider); + add(`${prefix}_MODEL_PROVIDER`, agent.modelProvider); + add(`${prefix}_MODEL`, agent.model); + add(`${prefix}_EFFORT`, agent.effort); + } + + const repository = configuration.repository; + add('MAIN_BRANCH', repository.mainBranch); + add('DEVELOPMENT_BRANCH', repository.developmentBranch); + add('FEATURE_TREE', repository.featureTree); + add('BUGFIX_TREE', repository.bugfixTree); + add('HOTFIX_TREE', repository.hotfixTree); + add('RELEASE_TREE', repository.releaseTree); + add('DOCS_TREE', repository.docsTree); + add('CHORE_TREE', repository.choreTree); + add('BRANCH_MANAGEMENT_ALWAYS', repository.branchManagementAlways); + add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); + add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); + add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); + add('MERGE_TIMEOUT', repository.mergeTimeout); + add('ISSUES_LOCALE', repository.issueLocale); + add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); + add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); + + add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); + add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); + add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); + add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); + add('BUGBOT_SEVERITY', configuration.ai.bugbotSeverity); + add('BUGBOT_COMMENT_LIMIT', configuration.ai.bugbotCommentLimit); + add('BUGBOT_AUTOFIX_VERIFY_COMMANDS', configuration.ai.bugbotFixVerifyCommands); + + add('PROJECT_IDS', configuration.projects.ids); + add('PROJECT_COLUMN_ISSUE_CREATED', configuration.projects.issueCreatedColumn); + add('PROJECT_COLUMN_PULL_REQUEST_CREATED', configuration.projects.pullRequestCreatedColumn); + add('PROJECT_COLUMN_ISSUE_IN_PROGRESS', configuration.projects.issueInProgressColumn); + add('PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS', configuration.projects.pullRequestInProgressColumn); + return variables; +} + +export function buildSetupActionInputs(configuration: SetupConfiguration): Record { + const repository = configuration.repository; + const ai = configuration.ai; + const projects = configuration.projects; + return { + 'main-branch': repository.mainBranch, + 'development-branch': repository.developmentBranch, + 'feature-tree': repository.featureTree, + 'bugfix-tree': repository.bugfixTree, + 'hotfix-tree': repository.hotfixTree, + 'release-tree': repository.releaseTree, + 'docs-tree': repository.docsTree, + 'chore-tree': repository.choreTree, + 'branch-management-always': String(repository.branchManagementAlways), + 'reopen-issue-on-push': String(repository.reopenIssueOnPush), + 'desired-assignees-count': String(repository.desiredAssigneesCount), + 'desired-reviewers-count': String(repository.desiredReviewersCount), + 'merge-timeout': String(repository.mergeTimeout), + 'issues-locale': repository.issueLocale, + 'pull-requests-locale': repository.pullRequestLocale, + 'commit-prefix-transforms': repository.commitPrefixTransforms, + 'ai-pull-request-description': String(ai.pullRequestDescription), + 'ai-ignore-files': ai.ignoreFiles, + 'ai-members-only': String(ai.membersOnly), + 'ai-include-reasoning': String(ai.includeReasoning), + 'bugbot-severity': ai.bugbotSeverity, + 'bugbot-comment-limit': String(ai.bugbotCommentLimit), + 'bugbot-fix-verify-commands': ai.bugbotFixVerifyCommands, + 'project-ids': projects.ids, + 'project-column-issue-created': projects.issueCreatedColumn, + 'project-column-pull-request-created': projects.pullRequestCreatedColumn, + 'project-column-issue-in-progress': projects.issueInProgressColumn, + 'project-column-pull-request-in-progress': projects.pullRequestInProgressColumn, + ...buildAgentActionInputs(configuration), + ...configuration.actionInputs, + }; +} + +function buildAgentActionInputs(configuration: SetupConfiguration): Record { + const result: Record = {}; + const base = configuration.agents.findings; + const add = (key: string, value: string | undefined) => { if (value !== undefined) result[key] = value; }; + add('agent-provider', base.provider); + add('agent-model-provider', base.modelProvider); + add('agent-model', base.model); + add('agent-effort', base.effort); + for (const task of SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + const prefix = `${task}-`; + add(`${prefix}provider`, agent.provider); + add(`${prefix}model-provider`, agent.modelProvider); + add(`${prefix}model`, agent.model); + add(`${prefix}effort`, agent.effort); + } + return result; +} + +function buildRequiredSetupSecrets(configuration: SetupConfiguration): string[] { + return buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); +} + +function buildSetupWarnings(configuration: SetupConfiguration): string[] { + const warnings: string[] = []; + if (configuration.features.release !== false && configuration.features.hotfix !== false) { + warnings.push('Release and hotfix workflows require the repository secret PAT and a writable token.'); + } + if (configuration.ai.provisioningMode === 'always') { + warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); + } + if (configuration.projects.ids.trim()) { + warnings.push('Project IDs must be accessible to the PAT and use the expected project column names.'); + } + if (SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { + warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); + } + return warnings; +} + +function unique(values: string[]): string[] { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} diff --git a/src/application/ports/setup_wizard_ports.ts b/src/application/ports/setup_wizard_ports.ts new file mode 100644 index 00000000..514b1085 --- /dev/null +++ b/src/application/ports/setup_wizard_ports.ts @@ -0,0 +1,72 @@ +import type { + SetupConfiguration, + SetupPlan, + SetupCredentialCheck, + SetupCredentialRequirement, + SetupCredentialDecision, + SetupCredentialValue, + SetupWorkflowComparison, + DoctorCheck, +} from '../../domain/setup'; + +export interface SetupPromptPort { + collect(defaults: SetupConfiguration): Promise; + showPlan(plan: SetupPlan): void; + confirm(plan: SetupPlan): Promise; + close(): void; +} + +export interface SetupCredentialPromptPort { + requestSetupPat(): Promise; + explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; + requestWorkflowPat(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + requestApiKey(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise; + chooseExistingCredential(requirement: SetupCredentialRequirement, check: SetupCredentialCheck): Promise; + showCredentialChecks(checks: readonly SetupCredentialCheck[]): void; +} + +export interface SetupRepositorySecretsPort { + list(owner: string, repository: string, token: string): Promise; + upsertSecrets( + owner: string, + repository: string, + token: string, + credentials: readonly SetupCredentialValue[], + ): Promise<{ created: number; updated: number; skipped: number; errors: string[] }>; +} + +export interface SetupRepositoryConfigurationReadPort { + listVariables(owner: string, repository: string, token: string): Promise; +} + +export interface DoctorOutputPort { + showDoctorChecks(checks: readonly DoctorCheck[]): void; +} + +export interface SetupCredentialValidationPort { + validateSetupPat(owner: string, repository: string, token: string): Promise; + validateCredential(requirement: SetupCredentialRequirement, value: string): Promise; +} + +export interface SetupRemoteCredentialHealthPort { + validateExisting( + owner: string, + repository: string, + token: string, + ref: string, + requirements: readonly SetupCredentialRequirement[], + ): Promise; +} + +export interface SetupWorkflowUpdatePromptPort { + confirmWorkflowUpdates(comparisons: readonly SetupWorkflowComparison[], forcedByFlag: boolean): Promise; +} + +export interface SetupRepositoryVariablesPort { + upsert( + owner: string, + repository: string, + token: string, + variables: readonly { name: string; value: string }[], + ): Promise<{ created: number; updated: number; errors: string[] }>; +} diff --git a/src/application/ports/setup_workspace_ports.ts b/src/application/ports/setup_workspace_ports.ts index 500cf51e..9cb9cfbc 100644 --- a/src/application/ports/setup_workspace_ports.ts +++ b/src/application/ports/setup_workspace_ports.ts @@ -1,9 +1,18 @@ +import type { SetupFeatures, SetupWorkflowComparison } from '../../domain/setup'; + export interface SetupWorkspaceResult { copied: number; skipped: number; } +export interface SetupWorkspaceSelection { + features?: SetupFeatures; + updateExistingWorkflows?: boolean; + approvedWorkflowFiles?: readonly string[]; +} + export interface SetupWorkspacePort { - prepare(): SetupWorkspaceResult; - hasValidToken(): boolean; + prepare(selection?: SetupWorkspaceSelection): SetupWorkspaceResult; + hasValidToken(tokenOverride?: string): boolean; + compareWorkflows?(features?: SetupFeatures): readonly SetupWorkflowComparison[]; } diff --git a/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts b/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts index 0061e1da..de9ccc71 100644 --- a/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts @@ -1,6 +1,7 @@ import { InitialSetupUseCase } from '../initial_setup_use_case'; import { Result } from '../../../../data/model/result'; import type { Execution } from '../../../../data/model/execution'; +import { createDefaultSetupConfiguration } from '../../../policies/setup_configuration_policy'; jest.mock('../../../../utils/logger', () => ({ logDebugInfo: jest.fn(), @@ -43,6 +44,7 @@ const mockEnsureInitialLabels = jest.fn(); const mockEnsureIssueTypes = jest.fn(); const mockSetupPrepare = jest.fn(); const mockSetupHasValidToken = jest.fn(); +const mockSetupVariablesUpsert = jest.fn(); function baseParam(overrides: Record = {}): Execution { return { @@ -85,6 +87,7 @@ describe('InitialSetupUseCase', () => { { getDefaultBranch: mockGetDefaultBranch } as any, { createTag: mockCreateTag } as any, { prepare: mockSetupPrepare, hasValidToken: mockSetupHasValidToken }, + { upsert: mockSetupVariablesUpsert }, ); mockSetupPrepare.mockReturnValue({ copied: 2, skipped: 0 }); mockSetupHasValidToken.mockReturnValue(true); @@ -102,6 +105,8 @@ describe('InitialSetupUseCase', () => { mockGetDefaultBranch.mockResolvedValue('main'); mockCreateTag.mockReset(); mockCreateTag.mockResolvedValue('abc123'); + mockSetupVariablesUpsert.mockReset(); + mockSetupVariablesUpsert.mockResolvedValue({ created: 1, updated: 2, errors: [] }); }); it('prepares the setup workspace and validates its token through the port', async () => { @@ -119,7 +124,7 @@ describe('InitialSetupUseCase', () => { expect(results).toHaveLength(1); expect(results[0].success).toBe(false); expect(results[0].errors.map((error) => error.message)).toContain( - 'PERSONAL_ACCESS_TOKEN must be set (environment or .env) with a valid token to run setup.' + 'A valid setup PAT must be provided to run setup. It is separate from the workflow PAT Secret.' ); expect(results[0].steps).not.toContainEqual( expect.stringMatching(/GitHub access verified/) @@ -160,6 +165,23 @@ describe('InitialSetupUseCase', () => { expect(mockCreateTag).toHaveBeenCalledWith('owner', 'repo', 'main', 'v1.0.0', 'token'); }); + it('applies the selected setup files and repository Variables from the wizard configuration', async () => { + const setupConfiguration = createDefaultSetupConfiguration(); + setupConfiguration.features.release = false; + setupConfiguration.createInitialTag = false; + const results = await useCase.invoke(baseParam({ inputs: { setupConfiguration } })); + + expect(results[0].success).toBe(true); + expect(mockSetupPrepare).toHaveBeenCalledWith({ features: setupConfiguration.features }); + expect(mockSetupVariablesUpsert).toHaveBeenCalledWith( + 'owner', + 'repo', + 'token', + expect.arrayContaining([{ name: 'AGENT_PROVIDER', value: 'codex' }]), + ); + expect(results[0].steps).toContain('⏭️ Initial version tag creation disabled by setup configuration.'); + }); + it('does not create default tag when repository already has tags', async () => { mockGetLatestTag.mockResolvedValue('2.0.0'); const param = baseParam(); diff --git a/src/application/usecases/actions/initial_setup_use_case.ts b/src/application/usecases/actions/initial_setup_use_case.ts index 342d555f..b9e248fe 100644 --- a/src/application/usecases/actions/initial_setup_use_case.ts +++ b/src/application/usecases/actions/initial_setup_use_case.ts @@ -7,6 +7,7 @@ import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '.. import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { ParamUseCase } from '../base/param_usecase'; import { runInitialSetupWorkflow } from './initial_setup_workflow'; +import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; /** Application boundary for provisioning a repository for Copilot automation. */ export class InitialSetupUseCase implements ParamUseCase { @@ -20,6 +21,8 @@ export class InitialSetupUseCase implements ParamUseCase { private readonly repositoryDefaultBranchPort: RepositoryDefaultBranchPort, private readonly repositoryTagPort: RepositoryTagPort, private readonly setupWorkspacePort: SetupWorkspacePort, + private readonly setupRepositoryVariablesPort?: SetupRepositoryVariablesPort, + private readonly setupRepositorySecretsPort?: SetupRepositorySecretsPort, ) {} async invoke(param: Execution): Promise { @@ -31,6 +34,8 @@ export class InitialSetupUseCase implements ParamUseCase { repositoryDefaultBranchPort: this.repositoryDefaultBranchPort, repositoryTagPort: this.repositoryTagPort, setupWorkspacePort: this.setupWorkspacePort, + setupRepositoryVariablesPort: this.setupRepositoryVariablesPort, + setupRepositorySecretsPort: this.setupRepositorySecretsPort, }); } } diff --git a/src/application/usecases/actions/initial_setup_workflow.ts b/src/application/usecases/actions/initial_setup_workflow.ts index 639fcb4a..6cb0afaa 100644 --- a/src/application/usecases/actions/initial_setup_workflow.ts +++ b/src/application/usecases/actions/initial_setup_workflow.ts @@ -12,6 +12,10 @@ import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { DEFAULT_INITIAL_TAG } from '../../../data/model/version_policy'; import { logDebugInfo, logError, logInfo } from '../../ports/logging_ports'; import { getTaskEmoji } from '../../../utils/task_emoji'; +import type { SetupConfiguration } from '../../../domain/setup'; +import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +import type { SetupCredentialCollection } from '../../../domain/setup'; +import { buildSetupRepositoryVariables } from '../../policies/setup_configuration_policy'; export interface InitialSetupWorkflowDependencies { authenticatedUserPort: AuthenticatedUserPort; @@ -21,6 +25,8 @@ export interface InitialSetupWorkflowDependencies { repositoryDefaultBranchPort: RepositoryDefaultBranchPort; repositoryTagPort: RepositoryTagPort; setupWorkspacePort: SetupWorkspacePort; + setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; + setupRepositorySecretsPort?: SetupRepositorySecretsPort; } type InitialLabelProvisioningOutcome = @@ -39,15 +45,23 @@ export async function runInitialSetupWorkflow( const errors: string[] = []; try { - logInfo('📋 Ensuring .github and copying setup files...'); - const filesResult = dependencies.setupWorkspacePort.prepare(); - steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); - if (!dependencies.setupWorkspacePort.hasValidToken()) { - logInfo(' 🛑 Setup requires PERSONAL_ACCESS_TOKEN (environment or .env) with a valid token.'); - errors.push('PERSONAL_ACCESS_TOKEN must be set (environment or .env) with a valid token to run setup.'); + const setupConfiguration = getSetupConfiguration(param); + if (!dependencies.setupWorkspacePort.hasValidToken(param.tokens.token)) { + logInfo(' 🛑 Setup requires the setup PAT provided for this command with a valid token.'); + errors.push('A valid setup PAT must be provided to run setup. It is separate from the workflow PAT Secret.'); return [buildResult(errors, steps)]; } - + logInfo('📋 Ensuring .github and copying setup files...'); + const workflowUpdates = getWorkflowUpdates(param); + const workspaceSelection = { + features: setupConfiguration?.features, + ...(workflowUpdates.length > 0 ? { + updateExistingWorkflows: true, + approvedWorkflowFiles: workflowUpdates, + } : {}), + }; + const filesResult = dependencies.setupWorkspacePort.prepare(workspaceSelection); + steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); logInfo('🔐 Checking GitHub access...'); const githubAccess = await verifyGitHubAccess(param, dependencies.authenticatedUserPort); if (!githubAccess.success) { @@ -56,6 +70,10 @@ export async function runInitialSetupWorkflow( } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); + const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration); + if (secrets.step) steps.push(secrets.step); + if (secrets.errors.length > 0) errors.push(...secrets.errors); + logInfo('🏷️ Checking configured and progress labels...'); const labels = await ensureInitialLabels(param, dependencies.initialLabelProvisioningPort); if (!labels.completed) { @@ -73,7 +91,11 @@ export async function runInitialSetupWorkflow( steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const defaultVersion = await ensureDefaultVersion(param, dependencies); + const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration); + if (variables.step) steps.push(variables.step); + if (variables.errors.length > 0) errors.push(...variables.errors); + + const defaultVersion = await ensureDefaultVersion(param, dependencies, setupConfiguration); if (defaultVersion.step) steps.push(defaultVersion.step); if (defaultVersion.error) errors.push(defaultVersion.error); return [buildResult(errors, steps)]; @@ -141,7 +163,11 @@ async function ensureIssueTypes( async function ensureDefaultVersion( param: Execution, dependencies: InitialSetupWorkflowDependencies, + setupConfiguration?: SetupConfiguration, ): Promise<{ step?: string; error?: string }> { + if (setupConfiguration?.createInitialTag === false) { + return { step: '⏭️ Initial version tag creation disabled by setup configuration.' }; + } try { const existingTag = await dependencies.latestTagQueryPort.getLatestTag(); if (existingTag !== undefined) { @@ -178,6 +204,87 @@ async function ensureDefaultVersion( } } +function getSetupConfiguration(param: Execution): SetupConfiguration | undefined { + const configuration = param.inputs?.setupConfiguration; + return configuration && typeof configuration === 'object' + ? configuration as SetupConfiguration + : undefined; +} + +function getWorkflowUpdates(param: Execution): string[] { + const updates = param.inputs?.setupWorkflowUpdates; + return Array.isArray(updates) ? updates.filter((file): file is string => typeof file === 'string') : []; +} + +async function ensureRepositoryVariables( + param: Execution, + dependencies: InitialSetupWorkflowDependencies, + setupConfiguration?: SetupConfiguration, +): Promise<{ step?: string; errors: string[] }> { + if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { + return { errors: [] }; + } + try { + const result = await dependencies.setupRepositoryVariablesPort.upsert( + param.owner, + param.repo, + param.tokens.token, + buildSetupRepositoryVariables(setupConfiguration), + ); + if (result.errors.length > 0) return { errors: result.errors }; + return { + step: `✅ Repository Variables: ${result.created} created, ${result.updated} updated`, + errors: [], + }; + } catch (error) { + const message = `Error configuring repository Variables: ${error}`; + logError(message); + return { errors: [message] }; + } +} + +async function ensureRepositorySecrets( + param: Execution, + dependencies: InitialSetupWorkflowDependencies, + setupConfiguration?: SetupConfiguration, +): Promise<{ step?: string; errors: string[] }> { + if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { + return { errors: [] }; + } + const credentials = getSetupCredentialCollection(param); + if (!credentials) { + return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; + } + const values = [ + ...(credentials.workflowPat ? [credentials.workflowPat] : []), + ...credentials.apiKeys, + ]; + if (values.length === 0) return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; + try { + const result = await dependencies.setupRepositorySecretsPort.upsertSecrets( + param.owner, + param.repo, + param.tokens.token, + values, + ); + if (result.errors.length > 0) return { errors: result.errors }; + return { + step: `✅ Repository Secrets: ${result.created} created, ${result.updated} updated; existing values kept when no replacement was selected.`, + errors: [], + }; + } catch (error) { + const message = `Error configuring repository Secrets: ${error}`; + logError(message); + return { errors: [message] }; + } +} + +function getSetupCredentialCollection(param: Execution): SetupCredentialCollection | undefined { + const credentials = param.inputs?.setupCredentials; + if (!credentials || typeof credentials !== 'object') return undefined; + return credentials as SetupCredentialCollection; +} + function appendLabelSummary( steps: string[], errors: string[], diff --git a/src/application/usecases/setup/__tests__/doctor_use_case.test.ts b/src/application/usecases/setup/__tests__/doctor_use_case.test.ts new file mode 100644 index 00000000..60c56fe3 --- /dev/null +++ b/src/application/usecases/setup/__tests__/doctor_use_case.test.ts @@ -0,0 +1,86 @@ +import { SetupDoctorUseCase } from '../doctor_use_case'; +import { buildSetupCredentialRequirements, buildSetupRepositoryVariables, createDefaultSetupConfiguration } from '../../../policies/setup_configuration_policy'; + +describe('SetupDoctorUseCase', () => { + function createDependencies(overrides: Record = {}) { + const output = { showDoctorChecks: jest.fn() }; + const dependencies = { + validation: { validateSetupPat: jest.fn().mockResolvedValue({ status: 'valid', message: 'ok' }), validateCredential: jest.fn() }, + secrets: { list: jest.fn().mockResolvedValue(['PAT', 'OPENAI_API_KEY', 'CODEX_ACCESS_TOKEN']), upsertSecrets: jest.fn() }, + variables: { listVariables: jest.fn().mockResolvedValue([]) }, + workspace: { prepare: jest.fn(), hasValidToken: jest.fn(), compareWorkflows: jest.fn().mockReturnValue([]) }, + ...overrides, + }; + return { output, dependencies }; + } + + it('reports missing variables/secrets and returns unhealthy without mutating', async () => { + const { output, dependencies } = createDependencies(); + const healthy = await new SetupDoctorUseCase( + dependencies.validation, + dependencies.secrets, + dependencies.variables, + dependencies.workspace, + output, + ).execute({ owner: 'owner', repository: 'repo', setupToken: 'token', configuration: createDefaultSetupConfiguration() }); + + expect(healthy).toBe(false); + expect(output.showDoctorChecks).toHaveBeenCalledWith(expect.arrayContaining([ + expect.objectContaining({ area: 'Variable AGENT_PROVIDER', status: 'fail' }), + expect.objectContaining({ area: 'Secret PAT', status: 'warn' }), + ])); + expect(dependencies.variables.listVariables).toHaveBeenCalledTimes(1); + }); + + it('reports valid remote health and matching variables as healthy', async () => { + const configuration = createDefaultSetupConfiguration(); + const variables = { listVariables: jest.fn() }; + variables.listVariables.mockResolvedValue(buildSetupRepositoryVariables(configuration)); + const requiredSecrets = buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); + const { output, dependencies } = createDependencies({ + variables, + secrets: { list: jest.fn().mockResolvedValue(requiredSecrets), upsertSecrets: jest.fn() }, + workspace: { prepare: jest.fn(), hasValidToken: jest.fn(), compareWorkflows: jest.fn().mockReturnValue([]) }, + }); + const remoteHealth = { + validateExisting: jest.fn().mockResolvedValue(requiredSecrets.map(name => ({ name, status: 'valid', message: 'remote ok' }))), + }; + const healthy = await new SetupDoctorUseCase( + dependencies.validation, + dependencies.secrets, + dependencies.variables, + dependencies.workspace, + output, + remoteHealth, + ).execute({ owner: 'owner', repository: 'repo', setupToken: 'token', configuration }); + expect(healthy).toBe(true); + expect(output.showDoctorChecks).toHaveBeenCalled(); + }); + + it('fails when an installed workflow or variable differs from the expected contract', async () => { + const configuration = createDefaultSetupConfiguration(); + const variables = { listVariables: jest.fn().mockResolvedValue([{ name: 'AGENT_PROVIDER', value: 'cursor' }]) }; + const { output, dependencies } = createDependencies({ + variables, + secrets: { list: jest.fn().mockResolvedValue([]), upsertSecrets: jest.fn() }, + workspace: { + prepare: jest.fn(), + hasValidToken: jest.fn(), + compareWorkflows: jest.fn().mockReturnValue([{ file: 'copilot_issue.yml', destination: '.github/workflows/copilot_issue.yml', status: 'changed' }]), + }, + }); + const healthy = await new SetupDoctorUseCase( + dependencies.validation, + dependencies.secrets, + dependencies.variables, + dependencies.workspace, + output, + ).execute({ owner: 'owner', repository: 'repo', setupToken: 'token', configuration }); + + expect(healthy).toBe(false); + expect(output.showDoctorChecks).toHaveBeenCalledWith(expect.arrayContaining([ + expect.objectContaining({ area: 'Workflow copilot_issue.yml', status: 'fail' }), + expect.objectContaining({ area: 'Variable AGENT_PROVIDER', status: 'fail' }), + ])); + }); +}); diff --git a/src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts b/src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts new file mode 100644 index 00000000..8035426b --- /dev/null +++ b/src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts @@ -0,0 +1,75 @@ +import { SetupCredentialsUseCase } from '../setup_credentials_use_case'; + +const requirement = (name: string, kind: 'workflowPat' | 'apiKey' = 'apiKey') => ({ name, kind, description: name, provider: 'openai' }); + +describe('SetupCredentialsUseCase', () => { + it('validates supplied new credentials and returns values only in memory', async () => { + const prompt = { + requestSetupPat: jest.fn(), explainCredentialSeparation: jest.fn(), + requestWorkflowPat: jest.fn().mockResolvedValue({ name: 'PAT', value: 'workflow-token' }), + requestApiKey: jest.fn().mockResolvedValue({ name: 'OPENAI_API_KEY', value: 'api-key' }), + chooseExistingCredential: jest.fn(), showCredentialChecks: jest.fn(), + }; + const validation = { + validateSetupPat: jest.fn().mockResolvedValue({ name: 'SETUP_PAT', status: 'valid', message: 'ok' }), + validateCredential: jest.fn().mockResolvedValue({ name: 'OPENAI_API_KEY', status: 'valid', message: 'ok' }), + }; + const secrets = { list: jest.fn().mockResolvedValue([]), upsertSecrets: jest.fn() }; + const result = await new SetupCredentialsUseCase(prompt, validation, secrets).collect({ + owner: 'owner', repository: 'repo', setupToken: 'setup-token', + requirements: [requirement('PAT', 'workflowPat'), requirement('OPENAI_API_KEY')], manageSecrets: true, + }); + + expect(result.collection).toEqual({ workflowPat: { name: 'PAT', value: 'workflow-token' }, apiKeys: [{ name: 'OPENAI_API_KEY', value: 'api-key' }] }); + expect(validation.validateSetupPat).toHaveBeenCalledWith('owner', 'repo', 'setup-token'); + expect(validation.validateCredential).toHaveBeenCalledWith(expect.objectContaining({ name: 'OPENAI_API_KEY' }), 'api-key'); + }); + + it('keeps an existing valid credential without requesting its value', async () => { + const prompt = { + requestSetupPat: jest.fn(), explainCredentialSeparation: jest.fn(), requestWorkflowPat: jest.fn(), requestApiKey: jest.fn(), + chooseExistingCredential: jest.fn().mockResolvedValue('keep'), showCredentialChecks: jest.fn(), + }; + const validation = { + validateSetupPat: jest.fn().mockResolvedValue({ name: 'SETUP_PAT', status: 'valid', message: 'ok' }), + validateCredential: jest.fn(), + }; + const secrets = { list: jest.fn().mockResolvedValue(['PAT']), upsertSecrets: jest.fn() }; + const remoteHealth = { validateExisting: jest.fn().mockResolvedValue([{ name: 'PAT', status: 'valid', message: 'remote ok' }]) }; + const result = await new SetupCredentialsUseCase(prompt, validation, secrets, remoteHealth).collect({ + owner: 'owner', repository: 'repo', setupToken: 'setup-token', ref: 'main', + requirements: [requirement('PAT', 'workflowPat')], manageSecrets: true, + }); + + expect(result.collection).toEqual({ apiKeys: [] }); + expect(prompt.requestWorkflowPat).not.toHaveBeenCalled(); + expect(remoteHealth.validateExisting).toHaveBeenCalledWith('owner', 'repo', 'setup-token', 'main', expect.any(Array)); + }); + + it('fails closed when a required credential is omitted', async () => { + const prompt = { + requestSetupPat: jest.fn(), explainCredentialSeparation: jest.fn(), requestWorkflowPat: jest.fn().mockResolvedValue(undefined), requestApiKey: jest.fn(), + chooseExistingCredential: jest.fn(), showCredentialChecks: jest.fn(), + }; + const validation = { validateSetupPat: jest.fn().mockResolvedValue({ name: 'SETUP_PAT', status: 'valid', message: 'ok' }), validateCredential: jest.fn() }; + const secrets = { list: jest.fn().mockResolvedValue([]), upsertSecrets: jest.fn() }; + await expect(new SetupCredentialsUseCase(prompt, validation, secrets).collect({ + owner: 'owner', repository: 'repo', setupToken: 'setup-token', requirements: [requirement('PAT', 'workflowPat')], manageSecrets: true, + })).rejects.toThrow('PAT is required'); + }); + + it('does not allow an invalid existing credential to be skipped', async () => { + const prompt = { + requestSetupPat: jest.fn(), explainCredentialSeparation: jest.fn(), requestWorkflowPat: jest.fn(), requestApiKey: jest.fn(), + chooseExistingCredential: jest.fn().mockResolvedValue('skip'), showCredentialChecks: jest.fn(), + }; + const validation = { validateSetupPat: jest.fn().mockResolvedValue({ name: 'SETUP_PAT', status: 'valid', message: 'ok' }), validateCredential: jest.fn() }; + const secrets = { list: jest.fn().mockResolvedValue(['OPENAI_API_KEY']), upsertSecrets: jest.fn() }; + const remoteHealth = { validateExisting: jest.fn().mockResolvedValue([{ name: 'OPENAI_API_KEY', status: 'invalid', message: 'remote rejected it' }]) }; + + await expect(new SetupCredentialsUseCase(prompt, validation, secrets, remoteHealth).collect({ + owner: 'owner', repository: 'repo', setupToken: 'setup-token', requirements: [requirement('OPENAI_API_KEY')], manageSecrets: true, + })).rejects.toThrow('OPENAI_API_KEY is invalid and must be replaced'); + expect(prompt.requestApiKey).not.toHaveBeenCalled(); + }); +}); diff --git a/src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts b/src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts new file mode 100644 index 00000000..6cec2f56 --- /dev/null +++ b/src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts @@ -0,0 +1,45 @@ +import { SetupWizardUseCase } from '../setup_wizard_use_case'; +import { createDefaultSetupConfiguration } from '../../../policies/setup_configuration_policy'; +import type { SetupPromptPort } from '../../../ports/setup_wizard_ports'; + +describe('SetupWizardUseCase', () => { + it('validates, previews, and confirms the collected configuration', async () => { + const prompt: jest.Mocked = { + collect: jest.fn(async defaults => defaults), + showPlan: jest.fn(), + confirm: jest.fn(async (_plan) => true), + close: jest.fn(), + }; + const useCase = new SetupWizardUseCase(prompt); + + const result = await useCase.collect(); + + expect(result).toEqual(createDefaultSetupConfiguration()); + expect(prompt.showPlan).toHaveBeenCalledTimes(1); + expect(prompt.confirm).toHaveBeenCalledTimes(1); + }); + + it('honors skipRepositoryVariables even if a prompt adapter returns true', async () => { + const prompt: jest.Mocked = { + collect: jest.fn(async defaults => ({ ...defaults, manageRepositoryVariables: true })), + showPlan: jest.fn(), + confirm: jest.fn(async (_plan) => true), + close: jest.fn(), + }; + + const result = await new SetupWizardUseCase(prompt).collect({ skipRepositoryVariables: true }); + + expect(result?.manageRepositoryVariables).toBe(false); + }); + + it('does not apply a plan when confirmation is declined', async () => { + const prompt: jest.Mocked = { + collect: jest.fn(async defaults => defaults), + showPlan: jest.fn(), + confirm: jest.fn(async (_plan) => false), + close: jest.fn(), + }; + + await expect(new SetupWizardUseCase(prompt).collect()).resolves.toBeUndefined(); + }); +}); diff --git a/src/application/usecases/setup/doctor_use_case.ts b/src/application/usecases/setup/doctor_use_case.ts new file mode 100644 index 00000000..d8ed143d --- /dev/null +++ b/src/application/usecases/setup/doctor_use_case.ts @@ -0,0 +1,80 @@ +import type { SetupConfiguration, DoctorCheck } from '../../../domain/setup'; +import { buildSetupCredentialRequirements, buildSetupRepositoryVariables } from '../../policies/setup_configuration_policy'; +import type { + DoctorOutputPort, + SetupCredentialValidationPort, + SetupRepositoryConfigurationReadPort, + SetupRepositorySecretsPort, + SetupRemoteCredentialHealthPort, +} from '../../ports/setup_wizard_ports'; +import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; + +export interface DoctorRequest { + owner: string; + repository: string; + setupToken: string; + configuration: SetupConfiguration; +} + +export class SetupDoctorUseCase { + constructor( + private readonly validation: SetupCredentialValidationPort, + private readonly secrets: SetupRepositorySecretsPort, + private readonly variables: SetupRepositoryConfigurationReadPort, + private readonly workspace: SetupWorkspacePort, + private readonly output: DoctorOutputPort, + private readonly remoteHealth?: SetupRemoteCredentialHealthPort, + ) {} + + async execute(request: DoctorRequest): Promise { + const checks: DoctorCheck[] = []; + const pat = await this.validation.validateSetupPat(request.owner, request.repository, request.setupToken); + checks.push({ area: 'Setup PAT', status: pat.status === 'valid' ? 'pass' : 'fail', message: pat.message }); + if (pat.status !== 'valid') { + this.output.showDoctorChecks(checks); + return false; + } + + const comparisons = this.workspace.compareWorkflows?.(request.configuration.features) ?? []; + for (const comparison of comparisons) { + checks.push({ + area: `Workflow ${comparison.file}`, + status: comparison.status === 'unchanged' ? 'pass' : 'fail', + message: comparison.status === 'unchanged' ? 'Matches the installed setup template.' : `Local workflow is ${comparison.status}.`, + }); + } + + const requiredVariables = buildSetupRepositoryVariables(request.configuration); + const remoteVariables = await this.variables.listVariables(request.owner, request.repository, request.setupToken); + const remoteVariableMap = new Map(remoteVariables.map(variable => [variable.name, variable.value])); + for (const variable of requiredVariables) { + const value = remoteVariableMap.get(variable.name); + checks.push({ + area: `Variable ${variable.name}`, + status: value === undefined ? 'fail' : value === variable.value ? 'pass' : 'fail', + message: value === undefined ? 'Variable is missing.' : value === variable.value ? 'Variable is configured.' : 'Variable exists but differs from the selected setup configuration.', + }); + } + + const remoteSecrets = new Set(await this.secrets.list(request.owner, request.repository, request.setupToken)); + const requirements = buildSetupCredentialRequirements(request.configuration); + const remoteHealth = this.remoteHealth + ? await this.remoteHealth.validateExisting(request.owner, request.repository, request.setupToken, request.configuration.repository.mainBranch, requirements.filter(requirement => remoteSecrets.has(requirement.name))) + : undefined; + const remoteHealthByName = new Map((remoteHealth ?? []).map(check => [check.name, check])); + for (const requirement of requirements) { + if (!remoteSecrets.has(requirement.name)) { + checks.push({ area: `Secret ${requirement.name}`, status: 'fail', message: 'Secret is missing.' }); + } else { + const health = remoteHealthByName.get(requirement.name); + checks.push({ + area: `Secret ${requirement.name}`, + status: health?.status === 'valid' ? 'pass' : health?.status === 'invalid' ? 'fail' : 'warn', + message: health?.message ?? 'Secret is present, but the remote credential health workflow is unavailable.', + }); + } + } + this.output.showDoctorChecks(checks); + return checks.every(check => check.status !== 'fail'); + } +} diff --git a/src/application/usecases/setup/index.ts b/src/application/usecases/setup/index.ts new file mode 100644 index 00000000..81102e29 --- /dev/null +++ b/src/application/usecases/setup/index.ts @@ -0,0 +1,4 @@ +export { SetupWizardUseCase } from './setup_wizard_use_case'; +export type { SetupWizardRequest } from './setup_wizard_use_case'; +export { SetupCredentialsUseCase } from './setup_credentials_use_case'; +export type { SetupCredentialsRequest, SetupCredentialsResult } from './setup_credentials_use_case'; diff --git a/src/application/usecases/setup/setup_credentials_use_case.ts b/src/application/usecases/setup/setup_credentials_use_case.ts new file mode 100644 index 00000000..3747e878 --- /dev/null +++ b/src/application/usecases/setup/setup_credentials_use_case.ts @@ -0,0 +1,109 @@ +import type { + SetupCredentialCheck, + SetupCredentialCollection, + SetupCredentialRequirement, + SetupCredentialValue, +} from '../../../domain/setup'; +import type { + SetupCredentialPromptPort, + SetupCredentialValidationPort, + SetupRepositorySecretsPort, + SetupRemoteCredentialHealthPort, +} from '../../ports/setup_wizard_ports'; + +export interface SetupCredentialsRequest { + owner: string; + repository: string; + setupToken: string; + requirements: readonly SetupCredentialRequirement[]; + manageSecrets: boolean; + ref?: string; +} + +export interface SetupCredentialsResult { + collection: SetupCredentialCollection; + checks: SetupCredentialCheck[]; + existingSecretNames: readonly string[]; +} + +/** Coordinates secret collection and validation without placing secret values in config files. */ +export class SetupCredentialsUseCase { + constructor( + private readonly prompt: SetupCredentialPromptPort, + private readonly validation: SetupCredentialValidationPort, + private readonly secrets?: SetupRepositorySecretsPort, + private readonly remoteHealth?: SetupRemoteCredentialHealthPort, + ) {} + + async collect(request: SetupCredentialsRequest): Promise { + const setupCheck = await this.validation.validateSetupPat(request.owner, request.repository, request.setupToken); + if (setupCheck.status !== 'valid') { + throw new Error(`Setup PAT validation failed: ${setupCheck.message}`); + } + if (!request.manageSecrets) { + this.prompt.showCredentialChecks([setupCheck]); + return { collection: { apiKeys: [] }, checks: [setupCheck], existingSecretNames: [] }; + } + if (!this.secrets) throw new Error('Repository Secret provisioning is not available in this installation.'); + + const existingSecretNames = await this.secrets.list(request.owner, request.repository, request.setupToken); + const requirements = request.requirements.filter(requirement => requirement.name !== 'SETUP_PAT'); + this.prompt.explainCredentialSeparation(requirements); + const existingRequirements = requirements.filter(requirement => existingSecretNames.includes(requirement.name)); + const remoteChecks = this.remoteHealth && existingRequirements.length > 0 + ? await this.remoteHealth.validateExisting( + request.owner, + request.repository, + request.setupToken, + request.ref ?? 'master', + existingRequirements, + ) + : undefined; + const remoteCheckByName = new Map((remoteChecks ?? []).map(check => [check.name, check])); + const checks: SetupCredentialCheck[] = [setupCheck]; + const values: SetupCredentialValue[] = []; + + for (const requirement of requirements) { + const existing = existingSecretNames.includes(requirement.name); + if (existing) { + const remoteCheck: SetupCredentialCheck = remoteCheckByName.get(requirement.name) ?? { + name: requirement.name, + status: 'unverifiable', + message: 'The remote health workflow is not available yet; GitHub does not reveal Secret values.', + }; + checks.push(remoteCheck); + const decision = await this.prompt.chooseExistingCredential(requirement, remoteCheck); + if (remoteCheck.status === 'invalid' && decision !== 'replace') { + throw new Error(`${requirement.name} is invalid and must be replaced before setup can continue.`); + } + if (decision === 'keep') continue; + if (decision === 'skip') continue; + } + + const value = requirement.kind === 'workflowPat' + ? await this.prompt.requestWorkflowPat(requirement, existing ? checks[checks.length - 1] : undefined) + : await this.prompt.requestApiKey(requirement, existing ? checks[checks.length - 1] : undefined); + if (!value) { + if (!existing) checks.push({ name: requirement.name, status: 'missing', message: 'No value was provided.' }); + throw new Error(`${requirement.name} is required by the selected workflows.`); + } + const check = requirement.kind === 'workflowPat' + ? await this.validation.validateSetupPat(request.owner, request.repository, value.value) + : await this.validation.validateCredential(requirement, value.value); + checks.push({ ...check, name: requirement.name }); + if (check.status !== 'valid') { + throw new Error(`${requirement.name} validation failed: ${check.message}`); + } + values.push(value); + } + this.prompt.showCredentialChecks(checks); + return { + collection: { + workflowPat: values.find(value => value.name === 'PAT'), + apiKeys: values.filter(value => value.name !== 'PAT'), + }, + checks, + existingSecretNames, + }; + } +} diff --git a/src/application/usecases/setup/setup_wizard_use_case.ts b/src/application/usecases/setup/setup_wizard_use_case.ts new file mode 100644 index 00000000..a0df36d0 --- /dev/null +++ b/src/application/usecases/setup/setup_wizard_use_case.ts @@ -0,0 +1,48 @@ +import type { SetupPromptPort } from '../../ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan } from '../../../domain/setup'; +import { + buildSetupPlan, + createDefaultSetupConfiguration, + mergeSetupConfiguration, + validateSetupConfiguration, + type SetupConfigurationOverrides, +} from '../../policies/setup_configuration_policy'; + +export interface SetupWizardRequest { + overrides?: SetupConfigurationOverrides; + skipRepositoryVariables?: boolean; +} + +export class SetupWizardUseCase { + constructor(private readonly prompt: SetupPromptPort) {} + + async collect(request: SetupWizardRequest = {}): Promise { + const defaults = mergeSetupConfiguration( + createDefaultSetupConfiguration(), + { + ...request.overrides, + ...(request.skipRepositoryVariables ? { manageRepositoryVariables: false } : {}), + }, + ); + const collected = await this.prompt.collect(defaults); + const configuration = request.skipRepositoryVariables + ? { ...collected, manageRepositoryVariables: false } + : collected; + const validationErrors = validateSetupConfiguration(configuration); + if (validationErrors.length > 0) { + throw new Error(`Invalid setup configuration:\n${validationErrors.map(error => `- ${error}`).join('\n')}`); + } + const plan = buildSetupPlan(configuration); + this.prompt.showPlan(plan); + if (!(await this.prompt.confirm(plan))) return undefined; + return configuration; + } + + plan(configuration: SetupConfiguration): SetupPlan { + return buildSetupPlan(configuration); + } + + close(): void { + this.prompt.close(); + } +} diff --git a/src/cli/__tests__/setup_config_file.test.ts b/src/cli/__tests__/setup_config_file.test.ts new file mode 100644 index 00000000..01709bef --- /dev/null +++ b/src/cli/__tests__/setup_config_file.test.ts @@ -0,0 +1,77 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { loadSetupConfigurationOverrides } from '../setup_config_file'; + +describe('setup configuration file loader', () => { + let directory: string; + + beforeEach(() => { + directory = mkdtempSync(join(tmpdir(), 'copilot-setup-config-')); + }); + + afterEach(() => { + rmSync(directory, { recursive: true, force: true }); + }); + + it('loads a typed YAML override object', () => { + const file = join(directory, 'setup.yml'); + writeFileSync(file, [ + 'features:', + ' release: false', + ' credentialHealth: true', + 'agents:', + ' reviewer:', + ' provider: opencode', + ' modelProvider: anthropic', + ' model: claude-3-7-sonnet', + 'repository:', + ' mainBranch: feature/token-refresh', + ' desiredReviewersCount: 2', + 'actionInputs:', + ' debug: "true"', + ].join('\n')); + + expect(loadSetupConfigurationOverrides(file)).toEqual({ + features: { release: false, credentialHealth: true }, + agents: { + reviewer: { + provider: 'opencode', + modelProvider: 'anthropic', + model: 'claude-3-7-sonnet', + }, + }, + repository: { mainBranch: 'feature/token-refresh', desiredReviewersCount: 2 }, + actionInputs: { debug: 'true' }, + }); + }); + + it('accepts non-secret credential management switches in the override file', () => { + const file = join(directory, 'setup.yml'); + writeFileSync(file, 'manageRepositorySecrets: true\nfeatures:\n credentialHealth: true\n'); + + expect(loadSetupConfigurationOverrides(file)).toEqual({ + manageRepositorySecrets: true, + features: { credentialHealth: true }, + }); + }); + + it('accepts JSON because JSON is a YAML-compatible document', () => { + const file = join(directory, 'setup.json'); + writeFileSync(file, JSON.stringify({ createInitialTag: false })); + + expect(loadSetupConfigurationOverrides(file)).toEqual({ createInitialTag: false }); + }); + + it.each([ + ['a secret-like value', '{"actionInputs":{"token":"secret"}}', /must not contain secrets/], + ['an unknown field', '{"reposotory":{"mainBranch":"main"}}', /Unknown setup configuration field/], + ['a wrong nested type', '{"repository":{"mergeTimeout":"600"}}', /repository\.mergeTimeout must be a non-negative integer/], + ['an unknown feature', '{"features":{"pulls":true}}', /Unknown features field/], + ])('rejects %s', (_name, content, error) => { + const file = join(directory, 'invalid.yml'); + writeFileSync(file, content); + + expect(() => loadSetupConfigurationOverrides(file)).toThrow(error); + }); +}); diff --git a/src/cli/__tests__/setup_prompt_adapter.test.ts b/src/cli/__tests__/setup_prompt_adapter.test.ts new file mode 100644 index 00000000..93b614be --- /dev/null +++ b/src/cli/__tests__/setup_prompt_adapter.test.ts @@ -0,0 +1,28 @@ +import { SetupPromptAdapter } from '../setup_prompt_adapter'; +import { buildSetupPlan, createDefaultSetupConfiguration } from '../../application/policies/setup_configuration_policy'; + +describe('SetupPromptAdapter non-interactive boundary', () => { + it('returns defaults and accepts explicit non-interactive decisions without reading secrets', async () => { + const adapter = new SetupPromptAdapter({ interactive: false, assumeYes: false, credentialValues: { PAT: 'workflow-token' } }); + const configuration = createDefaultSetupConfiguration(); + await expect(adapter.collect(configuration)).resolves.toBe(configuration); + const plan = buildSetupPlan(configuration); + adapter.showPlan(plan); + await expect(adapter.confirm(plan)).resolves.toBe(true); + await expect(adapter.requestSetupPat()).resolves.toBeUndefined(); + await expect(adapter.requestWorkflowPat(plan.credentialRequirements[0])).resolves.toEqual({ name: 'PAT', value: 'workflow-token' }); + await expect(adapter.requestApiKey(plan.credentialRequirements[1])).resolves.toBeUndefined(); + await expect(adapter.chooseExistingCredential(plan.credentialRequirements[0], { + name: 'PAT', status: 'unverifiable', message: 'unknown', + })).resolves.toBe('replace'); + await expect(adapter.confirmWorkflowUpdates([ + { file: 'workflow.yml', destination: '.github/workflows/workflow.yml', status: 'changed' }, + ], false)).resolves.toBe(false); + await expect(adapter.confirmWorkflowUpdates([ + { file: 'workflow.yml', destination: '.github/workflows/workflow.yml', status: 'changed' }, + ], true)).resolves.toBe(true); + adapter.showCredentialChecks([{ name: 'PAT', status: 'valid', message: 'ok' }]); + adapter.showDoctorChecks([{ area: 'PAT', status: 'pass', message: 'ok' }]); + adapter.close(); + }); +}); diff --git a/src/cli/cli_program.ts b/src/cli/cli_program.ts index c476188f..ba770e63 100644 --- a/src/cli/cli_program.ts +++ b/src/cli/cli_program.ts @@ -1,11 +1,8 @@ import { readFileSync } from 'node:fs'; import * as path from 'node:path'; -import * as dotenv from 'dotenv'; import { Command } from 'commander'; import { registerCliCommands } from './command_registry'; -dotenv.config(); - function loadPackageVersion(): string { const packagePath = path.join(__dirname, '..', '..', 'package.json'); const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) as { version?: unknown }; diff --git a/src/cli/command_registry.ts b/src/cli/command_registry.ts index 5a74f57c..c0c07e0d 100644 --- a/src/cli/command_registry.ts +++ b/src/cli/command_registry.ts @@ -6,6 +6,7 @@ import { registerRecommendStepsCommand } from './commands/recommend_steps'; import { registerDetectPotentialProblemsCommand } from './commands/detect_potential_problems'; import { registerSetupCommand } from './commands/setup'; import { registerUpgradeCommand } from './commands/upgrade'; +import { registerDoctorCommand } from './commands/doctor'; export function registerCliCommands(program: Command): Command { registerThinkCommand(program); @@ -15,5 +16,6 @@ export function registerCliCommands(program: Command): Command { registerDetectPotentialProblemsCommand(program); registerSetupCommand(program); registerUpgradeCommand(program); + registerDoctorCommand(program); return program; } diff --git a/src/cli/commands/doctor.ts b/src/cli/commands/doctor.ts new file mode 100644 index 00000000..d8f4b1e1 --- /dev/null +++ b/src/cli/commands/doctor.ts @@ -0,0 +1,44 @@ +import { Command } from 'commander'; +import { isInsideGitRepo, getGitInfo } from '../../cli_context'; +import { getSetupToken } from '../../utils/setup_files'; +import { logError, logInfo } from '../../utils/logger'; +import { SetupPromptAdapter } from '../setup_prompt_adapter'; +import { createSetupDoctorUseCase } from '../../infrastructure/composition/setup_doctor_composition_root'; +import { loadSetupConfigurationOverrides } from '../setup_config_file'; +import { createDefaultSetupConfiguration, mergeSetupConfiguration } from '../../application/policies/setup_configuration_policy'; + +export function registerDoctorCommand(program: Command): void { + program + .command('doctor') + .description('Verify Copilot workflows, Variables, Secrets, and setup PAT without changing repository configuration') + .option('-t, --token ', 'Setup PAT (or PERSONAL_ACCESS_TOKEN from the environment)') + .option('--config ', 'YAML or JSON setup configuration used as the expected contract') + .option('--non-interactive', 'Do not prompt; use --token or PERSONAL_ACCESS_TOKEN', false) + .action(async options => { + const prompt = new SetupPromptAdapter({ interactive: !options.nonInteractive }); + try { + const cwd = process.cwd(); + if (!isInsideGitRepo(cwd)) throw new Error('Run "copilot doctor" from the root of a git repository.'); + const gitInfo = getGitInfo(); + if ('error' in gitInfo) throw new Error(gitInfo.error); + let token = getSetupToken(cwd, options.token); + if (!token && !options.nonInteractive) token = await prompt.requestSetupPat(); + if (!token) throw new Error('A setup PAT is required. Use --token or PERSONAL_ACCESS_TOKEN. No .env file is supported.'); + const overrides = options.config ? loadSetupConfigurationOverrides(options.config) : {}; + const expected = mergeSetupConfiguration(createDefaultSetupConfiguration(), overrides); + logInfo(`🩺 Checking Copilot configuration for ${gitInfo.owner}/${gitInfo.repo}...`); + const healthy = await createSetupDoctorUseCase(prompt).execute({ + owner: gitInfo.owner, + repository: gitInfo.repo, + setupToken: token, + configuration: expected, + }); + if (!healthy) process.exitCode = 1; + } catch (error) { + logError(`Doctor failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + } finally { + prompt.close(); + } + }); +} diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index f43d2a7b..d613a5f9 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -1,46 +1,160 @@ import { Command } from 'commander'; import { runLocalAction } from '../../actions/local_action'; import { TITLE } from '../../utils/constants'; -import { getSetupToken, setupEnvFileExists } from '../../utils/setup_files'; +import { getSetupToken } from '../../utils/setup_files'; import { logError, logInfo } from '../../utils/logger'; import { getGitInfo, isInsideGitRepo } from '../../cli_context'; import { buildSetupParams } from './setup_policy'; +import { loadSetupConfigurationOverrides } from '../setup_config_file'; +import { SetupWizardUseCase } from '../../application/usecases/setup'; +import { SETUP_FEATURE_DESCRIPTIONS, buildSetupCredentialRequirements } from '../../application/policies/setup_configuration_policy'; +import type { SetupConfigurationOverrides } from '../../application/policies/setup_configuration_policy'; +import { createSetupCredentialsUseCase } from '../../infrastructure/composition/setup_credentials_composition_root'; +import { SetupWorkspaceAdapter } from '../../infrastructure/setup_workspace_adapter'; export function registerSetupCommand(program: Command): void { program .command('setup') - .description(`${TITLE} - Initial setup: create labels, issue types, and verify access`) + .description(`${TITLE} - Interactive repository setup: select workflows, agents, Variables, labels, and issue types`) .option('-d, --debug', 'Debug mode', false) .option('-t, --token ', 'Personal access token (or PERSONAL_ACCESS_TOKEN from the environment)') + .option('--agent ', 'Use one agent runtime for every setup task (codex|opencode|cursor)') + .option('--features ', 'Comma-separated setup features, or "all" (for non-interactive setup)') + .option('--config ', 'YAML or JSON file with setup overrides') + .option('--non-interactive', 'Use defaults and config-file values without prompting', false) + .option('--yes', 'Apply the plan without the final confirmation prompt', false) + .option('--dry-run', 'Show the setup plan without changing files or GitHub', false) + .option('--skip-variables', 'Do not create or update GitHub Repository Variables', false) + .option('--skip-secrets', 'Do not validate or create/update GitHub Repository Secrets', false) + .option('--update-workflows', 'Allow setup-managed workflows already in the repository to be updated', false) + .option('--workflow-pat ', 'Workflow PAT for the bot account (prefer the hidden interactive prompt)') + .option('--secret ', 'Secret value for non-interactive setup; repeat for each API key', collectSecret, {}) .action(async (options) => { + const { SetupPromptAdapter } = await import('../setup_prompt_adapter'); + const prompt = new SetupPromptAdapter({ + interactive: !options.nonInteractive, + assumeYes: Boolean(options.yes || options.nonInteractive || options.dryRun), + credentialValues: { + ...(options.workflowPat ? { PAT: options.workflowPat } : {}), + ...options.secret, + }, + }); const cwd = process.cwd(); - logInfo('🔍 Checking we are inside a git repository...'); - if (!isInsideGitRepo(cwd)) { - logError('❌ Not a git repository. Run "copilot setup" from the root of a git repo.'); - process.exit(1); + try { + logInfo('🔍 Checking we are inside a git repository...'); + if (!isInsideGitRepo(cwd)) { + logError('❌ Not a git repository. Run "copilot setup" from the root of a git repo.'); + process.exit(1); + return; + } + logInfo('✅ Git repository detected.'); + logInfo('🔗 Resolving repository (owner/repo)...'); + const gitInfo = getGitInfo(); + if ('error' in gitInfo) { + logError(gitInfo.error); + process.exit(1); + return; + } + logInfo(`📦 Repository: ${gitInfo.owner}/${gitInfo.repo}`); + let token = getSetupToken(cwd, options.token); + if (!token && !options.nonInteractive && !options.dryRun) token = await prompt.requestSetupPat(); + if (!token && !options.dryRun) { + logError('🛑 Setup requires PERSONAL_ACCESS_TOKEN with a valid token.'); + logInfo(' You can:'); + logInfo(' • Pass it on the command line: copilot setup --token '); + logInfo(' • Add it to your environment: export PERSONAL_ACCESS_TOKEN=your_github_token'); + process.exit(1); + return; + } + logInfo(options.dryRun ? '🧭 Building a dry-run setup plan...' : '🧭 Building your setup plan...'); + const wizard = new SetupWizardUseCase(prompt); + const overrides = loadSetupOverrides(options); + const configuration = await wizard.collect({ + overrides, + skipRepositoryVariables: Boolean(options.skipVariables), + }); + if (!configuration) { + logInfo('⏭️ Setup cancelled. No changes were applied.'); + return; + } + const workflowComparisons = new SetupWorkspaceAdapter().compareWorkflows(configuration.features); + const updateWorkflows = await prompt.confirmWorkflowUpdates(workflowComparisons, Boolean(options.updateWorkflows)); + const approvedWorkflowFiles = updateWorkflows + ? workflowComparisons.filter(comparison => comparison.status === 'changed').map(comparison => comparison.file) + : []; + if (options.dryRun) { + logInfo('✅ Dry run complete. No files or GitHub resources were changed.'); + return; + } + const credentials = await createSetupCredentialsUseCase(prompt).collect({ + owner: gitInfo.owner, + repository: gitInfo.repo, + setupToken: token ?? '', + requirements: buildSetupCredentialRequirements(configuration), + manageSecrets: !options.skipSecrets && configuration.manageRepositorySecrets, + ref: configuration.repository.mainBranch, + }); + logInfo('⚙️ Applying the approved setup plan...'); + const params = buildSetupParams(options, gitInfo, token ?? '', configuration, credentials.collection, approvedWorkflowFiles); + if (!params) return; + await runLocalAction(params); + } catch (error) { + logError(`Setup failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + } finally { + prompt.close(); } - logInfo('✅ Git repository detected.'); - logInfo('🔗 Resolving repository (owner/repo)...'); - const gitInfo = getGitInfo(); - if ('error' in gitInfo) { - logError(gitInfo.error); - process.exit(1); - } - logInfo(`📦 Repository: ${gitInfo.owner}/${gitInfo.repo}`); - const token = getSetupToken(cwd, options.token); - if (!token) { - logError('🛑 Setup requires PERSONAL_ACCESS_TOKEN with a valid token.'); - logInfo(' You can:'); - logInfo(' • Pass it on the command line: copilot setup --token '); - logInfo(' • Add it to your environment: export PERSONAL_ACCESS_TOKEN=your_github_token'); - if (setupEnvFileExists(cwd)) logInfo(' • Or add PERSONAL_ACCESS_TOKEN=your_github_token to your existing .env file'); - else logInfo(' • Or create a .env file in this repo with: PERSONAL_ACCESS_TOKEN=your_github_token'); - process.exit(1); - return; - } - logInfo('⚙️ Running initial setup (labels, issue types, access)...'); - const params = buildSetupParams(options, gitInfo, token); - if (!params) return; - await runLocalAction(params); }); } + +function collectSecret(value: string, previous: Record): Record { + const separator = value.indexOf('='); + if (separator <= 0) throw new Error('--secret must use NAME=VALUE syntax.'); + const name = value.slice(0, separator).trim(); + const secret = value.slice(separator + 1); + if (!/^[A-Z][A-Z0-9_]*$/.test(name) || !secret) throw new Error('--secret must use a non-empty NAME=VALUE with an uppercase secret name.'); + return { ...previous, [name]: secret }; +} + +function loadSetupOverrides(options: { + config?: string; + agent?: string; + features?: string; +}): SetupConfigurationOverrides { + const fromFile = options.config ? loadSetupConfigurationOverrides(options.config) : {}; + const fromFlags: SetupConfigurationOverrides = {}; + if (options.agent) { + if (!['codex', 'opencode', 'cursor'].includes(options.agent)) { + throw new Error('--agent must be one of: codex, opencode, cursor.'); + } + fromFlags.agents = Object.fromEntries( + ['planner', 'findings', 'reviewer', 'fixer', 'tester', 'release'].map(task => [task, { provider: options.agent }]), + ) as SetupConfigurationOverrides['agents']; + } + if (options.features) { + if (options.features.trim().toLowerCase() === 'all') { + fromFlags.features = Object.fromEntries(Object.keys(SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true])); + } else { + const requested = options.features.split(',').map(feature => feature.trim()).filter(Boolean); + const unknown = requested.filter(feature => !Object.prototype.hasOwnProperty.call(SETUP_FEATURE_DESCRIPTIONS, feature)); + if (unknown.length > 0) throw new Error(`Unknown setup feature(s): ${unknown.join(', ')}.`); + fromFlags.features = Object.fromEntries(Object.keys(SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, requested.includes(feature)])); + } + } + return mergeSetupOverrides(fromFile, fromFlags); +} + +function mergeSetupOverrides( + fileOverrides: SetupConfigurationOverrides, + flagOverrides: SetupConfigurationOverrides, +): SetupConfigurationOverrides { + return { + ...fileOverrides, + ...flagOverrides, + features: { ...fileOverrides.features, ...flagOverrides.features }, + agents: { ...fileOverrides.agents, ...flagOverrides.agents }, + repository: { ...fileOverrides.repository, ...flagOverrides.repository }, + ai: { ...fileOverrides.ai, ...flagOverrides.ai }, + projects: { ...fileOverrides.projects, ...flagOverrides.projects }, + }; +} diff --git a/src/cli/commands/setup_policy.ts b/src/cli/commands/setup_policy.ts index 8c154c78..c9874b72 100644 --- a/src/cli/commands/setup_policy.ts +++ b/src/cli/commands/setup_policy.ts @@ -1,5 +1,7 @@ import { ACTIONS, INPUT_KEYS } from '../../utils/constants'; import type { GitInfo } from '../../cli_context'; +import type { SetupConfiguration, SetupCredentialCollection } from '../../domain/setup'; +import { buildSetupActionInputs } from '../../application/policies/setup_configuration_policy'; export interface SetupCommandOptions { debug?: boolean; @@ -9,9 +11,13 @@ export function buildSetupParams( options: SetupCommandOptions, gitInfo: GitInfo, token: string, + configuration?: SetupConfiguration, + credentials?: SetupCredentialCollection, + approvedWorkflowFiles: readonly string[] = [], ): Record | undefined { if ('error' in gitInfo) return undefined; return { + ...(configuration ? buildSetupActionInputs(configuration) : {}), [INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', [INPUT_KEYS.SINGLE_ACTION]: ACTIONS.INITIAL_SETUP, [INPUT_KEYS.SINGLE_ACTION_ISSUE]: 1, @@ -21,7 +27,10 @@ export function buildSetupParams( [INPUT_KEYS.WELCOME_TITLE]: '⚙️ Initial Setup', [INPUT_KEYS.WELCOME_MESSAGES]: [ `Running initial setup for ${gitInfo.owner}/${gitInfo.repo}...`, - 'This will create labels, issue types, and verify access to GitHub.', + 'This will install the selected workflows, configure repository Variables, create labels and issue types, and verify access to GitHub.', ], + ...(configuration ? { setupConfiguration: configuration } : {}), + ...(credentials ? { setupCredentials: credentials } : {}), + setupWorkflowUpdates: approvedWorkflowFiles, }; } diff --git a/src/cli/setup_config_file.ts b/src/cli/setup_config_file.ts new file mode 100644 index 00000000..567b422e --- /dev/null +++ b/src/cli/setup_config_file.ts @@ -0,0 +1,140 @@ +import { readFileSync } from 'node:fs'; +import * as yaml from 'js-yaml'; +import { + SETUP_AGENT_TASKS, + SETUP_FEATURE_DESCRIPTIONS, + type SetupConfigurationOverrides, +} from '../application/policies/setup_configuration_policy'; + +const SETUP_OVERRIDE_KEYS = new Set([ + 'features', + 'agents', + 'repository', + 'ai', + 'projects', + 'createInitialTag', + 'manageRepositoryVariables', + 'manageRepositorySecrets', + 'actionInputs', +]); +const AGENT_OVERRIDE_KEYS = new Set(['provider', 'modelProvider', 'model', 'effort']); +const REPOSITORY_STRING_KEYS = new Set([ + 'mainBranch', + 'developmentBranch', + 'featureTree', + 'bugfixTree', + 'hotfixTree', + 'releaseTree', + 'docsTree', + 'choreTree', + 'issueLocale', + 'pullRequestLocale', + 'commitPrefixTransforms', +]); +const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush']); +const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout']); +const AI_BOOLEAN_KEYS = new Set(['pullRequestDescription', 'membersOnly', 'includeReasoning']); +const AI_STRING_KEYS = new Set(['ignoreFiles', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); +const AI_NUMBER_KEYS = new Set(['bugbotCommentLimit']); +const PROJECT_KEYS = new Set([ + 'ids', + 'issueCreatedColumn', + 'pullRequestCreatedColumn', + 'issueInProgressColumn', + 'pullRequestInProgressColumn', +]); + +/** Loads a non-secret setup override file. JSON and YAML are supported. */ +export function loadSetupConfigurationOverrides(filePath: string): SetupConfigurationOverrides { + const parsed = yaml.load(readFileSync(filePath, 'utf8')); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Setup configuration must be a YAML or JSON object.'); + } + const raw = parsed as Record; + if (containsCredentialMaterial(raw)) { + throw new Error('Setup configuration must not contain secrets or credential material.'); + } + validateObjectKeys(raw, SETUP_OVERRIDE_KEYS, 'setup configuration'); + validateOptionalObject(raw.features, 'features'); + if (raw.features !== undefined) { + validateObjectKeys(raw.features as Record, new Set(Object.keys(SETUP_FEATURE_DESCRIPTIONS)), 'features'); + validateBooleanValues(raw.features as Record, 'features'); + } + validateOptionalObject(raw.agents, 'agents'); + if (raw.agents !== undefined) { + const agents = raw.agents as Record; + validateObjectKeys(agents, new Set(SETUP_AGENT_TASKS), 'agents'); + for (const [task, value] of Object.entries(agents)) { + validateObject(value, `agents.${task}`); + const agent = value as Record; + validateObjectKeys(agent, AGENT_OVERRIDE_KEYS, `agents.${task}`); + validateStringValues(agent, `agents.${task}`); + } + } + validateSection(raw.repository, 'repository', REPOSITORY_STRING_KEYS, REPOSITORY_BOOLEAN_KEYS, REPOSITORY_NUMBER_KEYS); + validateSection(raw.ai, 'ai', AI_STRING_KEYS, AI_BOOLEAN_KEYS, AI_NUMBER_KEYS); + validateSection(raw.projects, 'projects', PROJECT_KEYS, new Set(), new Set()); + validateBooleanProperty(raw, 'createInitialTag'); + validateBooleanProperty(raw, 'manageRepositoryVariables'); + validateBooleanProperty(raw, 'manageRepositorySecrets'); + validateOptionalObject(raw.actionInputs, 'actionInputs'); + if (raw.actionInputs !== undefined) validateStringValues(raw.actionInputs as Record, 'actionInputs'); + return raw as SetupConfigurationOverrides; +} + +function validateSection( + value: unknown, + name: string, + stringKeys: ReadonlySet, + booleanKeys: ReadonlySet, + numberKeys: ReadonlySet, +): void { + if (value === undefined) return; + validateObject(value, name); + const section = value as Record; + validateObjectKeys(section, new Set([...stringKeys, ...booleanKeys, ...numberKeys]), name); + for (const key of stringKeys) if (section[key] !== undefined && typeof section[key] !== 'string') throw new Error(`${name}.${key} must be a string.`); + for (const key of booleanKeys) if (section[key] !== undefined && typeof section[key] !== 'boolean') throw new Error(`${name}.${key} must be a boolean.`); + for (const key of numberKeys) if (section[key] !== undefined && (!Number.isInteger(section[key]) || (section[key] as number) < 0)) throw new Error(`${name}.${key} must be a non-negative integer.`); +} + +function validateOptionalObject(value: unknown, name: string): void { + if (value !== undefined) validateObject(value, name); +} + +function validateObject(value: unknown, name: string): asserts value is Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${name} must be an object.`); +} + +function validateObjectKeys(value: Record, allowed: ReadonlySet, name: string): void { + const unknown = Object.keys(value).filter(key => !allowed.has(key)); + if (unknown.length > 0) throw new Error(`Unknown ${name} field(s): ${unknown.join(', ')}.`); +} + +function validateBooleanValues(value: Record, name: string): void { + for (const [key, item] of Object.entries(value)) if (typeof item !== 'boolean') throw new Error(`${name}.${key} must be a boolean.`); +} + +function validateStringValues(value: Record, name: string): void { + for (const [key, item] of Object.entries(value)) if (typeof item !== 'string') throw new Error(`${name}.${key} must be a string.`); +} + +function validateBooleanProperty(value: Record, key: string): void { + if (value[key] !== undefined && typeof value[key] !== 'boolean') throw new Error(`${key} must be a boolean.`); +} + +function containsCredentialMaterial(value: unknown): boolean { + if (typeof value === 'string') { + return /^(?:github_pat_|gh[pso]_|ghu_|ghs_|sk-|AIza|xox[baprs]-)/i.test(value.trim()); + } + if (!value || typeof value !== 'object') return false; + if (Array.isArray(value)) return value.some(containsCredentialMaterial); + return Object.entries(value).some(([key, item]) => { + // Boolean configuration switches such as `manageRepositorySecrets` and + // `features.credentialHealth` are not credential material. Only reject + // credential-shaped properties when they actually carry a value. + const looksLikeCredentialProperty = /(?:password|secret|token|api[_-]?key|credential)/i.test(key); + return (looksLikeCredentialProperty && item !== undefined && item !== null && typeof item !== 'boolean') + || containsCredentialMaterial(item); + }); +} diff --git a/src/cli/setup_prompt_adapter.ts b/src/cli/setup_prompt_adapter.ts new file mode 100644 index 00000000..9f9107ee --- /dev/null +++ b/src/cli/setup_prompt_adapter.ts @@ -0,0 +1,349 @@ +import { createInterface, type Interface } from 'node:readline/promises'; +import { stdin, stdout } from 'node:process'; +import type { + SetupCredentialPromptPort, + SetupPromptPort, + SetupWorkflowUpdatePromptPort, + DoctorOutputPort, +} from '../application/ports/setup_wizard_ports'; +import { + SETUP_AGENT_TASKS, + SETUP_FEATURE_DESCRIPTIONS, +} from '../application/policies/setup_configuration_policy'; +import type { + SetupAgentRoleConfiguration, + SetupConfiguration, + SetupPlan, + SetupCredentialCheck, + SetupCredentialRequirement, + SetupCredentialDecision, + SetupCredentialValue, + SetupWorkflowComparison, +} from '../domain/setup'; + +const AGENT_PROVIDERS = ['codex', 'opencode', 'cursor'] as const; +const MODEL_PROVIDERS = ['openai', 'anthropic', 'google', 'openrouter', 'opencode', 'local'] as const; + +export interface SetupPromptAdapterOptions { + interactive?: boolean; + assumeYes?: boolean; + credentialValues?: Record; +} + +export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { + private readonly interactive: boolean; + private readonly assumeYes: boolean; + private readonly readline: Interface | undefined; + private readonly credentialValues: Readonly>; + + constructor(options: SetupPromptAdapterOptions = {}) { + this.interactive = Boolean( + (options.interactive ?? Boolean(stdin.isTTY && stdout.isTTY)) + && stdin.isTTY + && stdout.isTTY + && !process.env.JEST_WORKER_ID, + ); + this.assumeYes = options.assumeYes ?? false; + this.credentialValues = options.credentialValues ?? {}; + this.readline = this.interactive ? createInterface({ input: stdin, output: stdout }) : undefined; + } + + async collect(defaults: SetupConfiguration): Promise { + if (!this.readline) return defaults; + console.log(renderBox( + 'This wizard configures repository workflows, GitHub Variables, GitHub Secrets, AI agents, and operational defaults.\n\nThe setup PAT is an operator credential used only during this command. It is different from the workflow PAT that the bot account uses at runtime.', + 'Copilot Setup', + )); + console.log(color('\n1. Choose the capabilities to install\n', 36)); + for (const [feature, description] of Object.entries(SETUP_FEATURE_DESCRIPTIONS)) { + defaults.features[feature] = await this.askBoolean(description, defaults.features[feature] !== false); + } + + console.log(color('\n2. Choose one of the three supported agent runtimes for each task\n', 36)); + for (const task of SETUP_AGENT_TASKS) { + defaults.agents[task].provider = await this.askChoice( + `${formatTask(task)} runtime`, + [...AGENT_PROVIDERS], + defaults.agents[task].provider, + ) as SetupAgentRoleConfiguration['provider']; + } + const modelProvider = await this.askChoice('Model provider for all tasks', [...MODEL_PROVIDERS], defaults.agents.findings.modelProvider); + const model = await this.askText('Model name for all tasks', defaults.agents.findings.model); + const effort = await this.askText('Reasoning effort for all tasks (leave empty for provider default)', defaults.agents.findings.effort ?? ''); + for (const task of SETUP_AGENT_TASKS) { + defaults.agents[task].modelProvider = modelProvider; + defaults.agents[task].model = model; + defaults.agents[task].effort = effort; + } + if (await this.askBoolean('Configure model provider, model, and effort independently for every task?', false)) { + for (const task of SETUP_AGENT_TASKS) { + defaults.agents[task].modelProvider = await this.askText(`${formatTask(task)} model provider`, defaults.agents[task].modelProvider); + defaults.agents[task].model = await this.askText(`${formatTask(task)} model`, defaults.agents[task].model); + defaults.agents[task].effort = await this.askText(`${formatTask(task)} effort (empty for default)`, defaults.agents[task].effort ?? ''); + } + } + + console.log(color('\n3. Configure repository behavior\n', 36)); + const repository = defaults.repository; + repository.mainBranch = await this.askText('Production branch', repository.mainBranch); + repository.developmentBranch = await this.askText('Development branch', repository.developmentBranch); + repository.featureTree = await this.askText('Feature branch prefix', repository.featureTree); + repository.bugfixTree = await this.askText('Bugfix branch prefix', repository.bugfixTree); + repository.hotfixTree = await this.askText('Hotfix branch prefix', repository.hotfixTree); + repository.releaseTree = await this.askText('Release branch prefix', repository.releaseTree); + repository.docsTree = await this.askText('Documentation branch prefix', repository.docsTree); + repository.choreTree = await this.askText('Chore branch prefix', repository.choreTree); + repository.branchManagementAlways = await this.askBoolean('Create/manage branches without requiring the branched label?', repository.branchManagementAlways); + repository.reopenIssueOnPush = await this.askBoolean('Reopen closed issues when a related branch receives a push?', repository.reopenIssueOnPush); + repository.desiredAssigneesCount = await this.askNumber('Desired issue assignees (0 disables automatic assignment)', repository.desiredAssigneesCount); + repository.desiredReviewersCount = await this.askNumber('Desired pull-request reviewers (0 disables automatic assignment)', repository.desiredReviewersCount); + repository.mergeTimeout = await this.askNumber('Merge timeout in seconds (0 disables the timeout)', repository.mergeTimeout); + repository.issueLocale = await this.askText('Issue comment locale', repository.issueLocale); + repository.pullRequestLocale = await this.askText('Pull-request comment locale', repository.pullRequestLocale); + repository.commitPrefixTransforms = await this.askText('Commit prefix transforms', repository.commitPrefixTransforms); + + console.log(color('\n4. Configure AI, projects, and release safety\n', 36)); + const ai = defaults.ai; + ai.pullRequestDescription = await this.askBoolean('Generate AI pull-request descriptions?', ai.pullRequestDescription); + ai.ignoreFiles = await this.askText('AI ignore file patterns (comma-separated)', ai.ignoreFiles); + ai.membersOnly = await this.askBoolean('Restrict AI processing to repository members?', ai.membersOnly); + ai.includeReasoning = await this.askBoolean('Include agent reasoning where supported?', ai.includeReasoning); + ai.bugbotSeverity = await this.askChoice('Minimum Bugbot severity to publish', ['info', 'low', 'medium', 'high'], ai.bugbotSeverity) as SetupConfiguration['ai']['bugbotSeverity']; + ai.bugbotCommentLimit = await this.askNumber('Maximum Bugbot comments per run', ai.bugbotCommentLimit); + ai.bugbotFixVerifyCommands = await this.askText('Bugbot autofix verification commands (comma-separated, empty is allowed)', ai.bugbotFixVerifyCommands); + ai.provisioningMode = await this.askChoice('Agent CLI provisioning mode', ['auto', 'always', 'disabled'], ai.provisioningMode) as SetupConfiguration['ai']['provisioningMode']; + defaults.projects.ids = await this.askText('GitHub Project IDs (comma-separated, empty to skip Projects integration)', defaults.projects.ids); + if (defaults.projects.ids.trim()) { + defaults.projects.issueCreatedColumn = await this.askText('Project column for new issues', defaults.projects.issueCreatedColumn); + defaults.projects.pullRequestCreatedColumn = await this.askText('Project column for new pull requests', defaults.projects.pullRequestCreatedColumn); + defaults.projects.issueInProgressColumn = await this.askText('Project column for issues in progress', defaults.projects.issueInProgressColumn); + defaults.projects.pullRequestInProgressColumn = await this.askText('Project column for pull requests in progress', defaults.projects.pullRequestInProgressColumn); + } + defaults.createInitialTag = await this.askBoolean('Create v1.0.0 when the repository has no version tags?', defaults.createInitialTag); + defaults.manageRepositoryVariables = await this.askBoolean('Create/update the non-sensitive GitHub Repository Variables used by the workflows?', defaults.manageRepositoryVariables); + defaults.manageRepositorySecrets = await this.askBoolean('Validate and provision the GitHub Secrets required by the selected workflows?', defaults.manageRepositorySecrets); + return defaults; + } + + showPlan(plan: SetupPlan): void { + const enabledFeatures = Object.entries(plan.configuration.features) + .filter(([, enabled]) => enabled) + .map(([feature]) => ` ${color('✓', 32)} ${SETUP_FEATURE_DESCRIPTIONS[feature] ?? feature}`) + .join('\n'); + const agents = SETUP_AGENT_TASKS + .map(task => ` ${formatTask(task)}: ${plan.configuration.agents[task].provider} / ${plan.configuration.agents[task].modelProvider}/${plan.configuration.agents[task].model}`) + .join('\n'); + const content = [ + color('Capabilities', 36), enabledFeatures || ' (none)', '', + color('Agent routing', 36), agents, '', + color('Repository changes', 36), + ` Files selected: ${plan.selectedFiles.length}`, + ` Variables to upsert: ${plan.configuration.manageRepositoryVariables ? plan.variables.length : 0}`, + ` Secrets to validate/provision: ${plan.configuration.manageRepositorySecrets ? plan.credentialRequirements.length : 0}`, + ` Labels and issue types: always checked by Copilot setup`, + ` Initial tag: ${plan.configuration.createInitialTag ? 'v1.0.0 when no version tag exists' : 'disabled'}`, '', + color('Credential contract', 33), ` ${plan.requiredSecrets.join(', ')}`, + ...(plan.warnings.length > 0 ? ['', color('Important notes', 33), ...plan.warnings.map(warning => ` ⚠ ${warning}`)] : []), + ].join('\n'); + console.log(renderBox(content, 'Setup Plan', 32)); + } + + async confirm(plan: SetupPlan): Promise { + if (this.assumeYes || !this.readline) return true; + return this.askBoolean(`Apply this setup plan to ${plan.configuration.manageRepositoryVariables ? 'the repository and GitHub Variables' : 'the repository'}?`, false); + } + + async requestSetupPat(): Promise { + if (!this.readline) return undefined; + console.log(renderBox( + 'Enter a GitHub setup PAT. It is used in memory for this run only and is never stored in the repository, a .env file, or a GitHub Secret.\n\nRecommended fine-grained permissions for the selected setup features:\n Repository: Metadata read, Contents read, Issues write, Actions read/write, Variables write, Secrets read/write, Workflows read/write.\n Organization: Issue Types write and Projects read/write only when selected; Members read when member-only checks are enabled.\n Contents write and Workflows write are needed only when changing workflow files through the GitHub API.\n\nThe workflow PAT is a different bot-account token and is requested separately.', + 'Setup PAT', + 33, + )); + return this.askSecret('Setup PAT'); + } + + explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void { + if (!this.readline) return; + console.log(renderBox( + 'The workflow PAT is not the setup PAT. The workflow PAT belongs to the bot account, is stored remotely as the PAT Secret, and is used by GitHub Actions to work on issues and pull requests. Existing Secrets are never readable through GitHub; Copilot can only validate them through the repository health workflow.', + 'Workflow credentials', + 33, + )); + console.log(`Required credentials: ${requirements.map(requirement => requirement.name).join(', ')}`); + } + + async requestWorkflowPat(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise { + return this.requestSecretForRequirement(requirement, current, 'workflow PAT owned by the bot account'); + } + + async requestApiKey(requirement: SetupCredentialRequirement, current?: SetupCredentialCheck): Promise { + return this.requestSecretForRequirement(requirement, current, `${requirement.provider ?? 'provider'} API key`); + } + + async chooseExistingCredential(requirement: SetupCredentialRequirement, check: SetupCredentialCheck): Promise { + if (this.credentialValues[requirement.name]?.trim()) return 'replace'; + if (!this.readline) return 'keep'; + console.log(`Existing ${requirement.name}: ${check.status}. ${check.message}`); + return this.askChoice( + `How should Copilot handle the existing ${requirement.name}?`, + ['keep', 'replace', 'skip'], + check.status === 'valid' ? 'keep' : 'replace', + ) as Promise; + } + + showCredentialChecks(checks: readonly SetupCredentialCheck[]): void { + if (checks.length === 0) return; + console.log(renderBox( + checks.map(check => ` ${statusIcon(check.status)} ${check.name}: ${check.status} — ${check.message}`).join('\n'), + 'Credential validation', + checks.some(check => check.status === 'invalid') ? 31 : 32, + )); + } + + showDoctorChecks(checks: readonly import('../domain/setup').DoctorCheck[]): void { + const content = checks.map(check => ` ${doctorIcon(check.status)} ${check.area}: ${check.message}`).join('\n'); + console.log(renderBox(content || ' No checks were available.', 'Copilot Doctor', checks.some(check => check.status === 'fail') ? 31 : 32)); + } + + async confirmWorkflowUpdates(comparisons: readonly SetupWorkflowComparison[], forcedByFlag: boolean): Promise { + const changed = comparisons.filter(comparison => comparison.status === 'changed' || comparison.status === 'unmanaged'); + if (changed.length === 0) return false; + if (!this.readline) return forcedByFlag; + console.log(renderBox( + changed.map(comparison => ` ${comparison.status === 'changed' ? '↻' : '⚠'} ${comparison.destination} (${comparison.status})`).join('\n'), + 'Existing workflows detected', + 33, + )); + if (forcedByFlag) { + console.log('The --update-workflows flag was provided; these setup-managed workflows are eligible for update.'); + return true; + } + return this.askBoolean('Update the detected workflows with the configuration selected in this setup?', false); + } + + close(): void { + this.readline?.close(); + } + + private async askText(question: string, defaultValue: string): Promise { + const answer = await this.readline!.question(`${question} ${color(`[${defaultValue || 'none'}]`, 90)}: `); + return answer.trim() || defaultValue; + } + + private async requestSecretForRequirement( + requirement: SetupCredentialRequirement, + current: SetupCredentialCheck | undefined, + label: string, + ): Promise { + const supplied = this.credentialValues[requirement.name]?.trim(); + if (supplied) return { name: requirement.name, value: supplied }; + if (!this.readline) return undefined; + if (current) { + console.log(`${requirement.name}: ${current.status} (${current.message})`); + } + const value = await this.askSecret(`${requirement.name} — ${label}`); + return value ? { name: requirement.name, value } : undefined; + } + + private async askSecret(question: string): Promise { + const input = stdin as typeof stdin & { setRawMode?: (mode: boolean) => void }; + if (!input.isTTY || !input.setRawMode) { + return (await this.readline!.question(`${question}: `)).trim(); + } + stdout.write(`${question}: `); + input.setRawMode(true); + input.resume(); + return await new Promise((resolve, reject) => { + let value = ''; + const onData = (chunk: Buffer | string) => { + const text = chunk.toString(); + for (const character of text) { + if (character === '\u0003') { + cleanup(); + reject(new Error('Input cancelled.')); + } else if (character === '\r' || character === '\n') { + cleanup(); + stdout.write('\n'); + resolve(value.trim()); + } else if (character === '\u007f') { + value = value.slice(0, -1); + } else { + value += character; + } + } + }; + const cleanup = () => { + input.off('data', onData); + input.setRawMode?.(false); + input.pause(); + }; + input.on('data', onData); + }); + } + + private async askNumber(question: string, defaultValue: number): Promise { + while (true) { + const value = await this.askText(question, String(defaultValue)); + const parsed = Number(value); + if (Number.isInteger(parsed) && parsed >= 0) return parsed; + console.log(color('Please enter a non-negative whole number.', 33)); + } + } + + private async askBoolean(question: string, defaultValue: boolean): Promise { + const answer = await this.readline!.question(`${question} ${color(`[${defaultValue ? 'Y' : 'N'}]`, 90)}: `); + const normalized = answer.trim().toLowerCase(); + if (!normalized) return defaultValue; + return ['y', 'yes', 'true'].includes(normalized); + } + + private async askChoice(question: string, choices: readonly string[], defaultValue: string): Promise { + console.log(question); + choices.forEach((choice, index) => console.log(` ${index + 1}) ${choice}${choice === defaultValue ? color(' (default)', 90) : ''}`)); + while (true) { + const answer = await this.readline!.question(`Select 1-${choices.length} ${color(`[${choices.indexOf(defaultValue) + 1}]`, 90)}: `); + if (!answer.trim()) return defaultValue; + const index = Number(answer) - 1; + if (Number.isInteger(index) && choices[index]) return choices[index]; + console.log(color('Please select one of the listed options.', 33)); + } + } +} + +function statusIcon(status: SetupCredentialCheck['status']): string { + if (status === 'valid') return '✓'; + if (status === 'unverifiable') return '?'; + if (status === 'missing') return '!'; + if (status === 'not_required') return '–'; + return '✗'; +} + +function doctorIcon(status: import('../domain/setup').DoctorCheckStatus): string { + return status === 'pass' ? '✓' : status === 'warn' ? '⚠' : '✗'; +} + +function formatTask(task: string): string { + return task.charAt(0).toUpperCase() + task.slice(1); +} + +function color(value: string, code: number): string { + if (!stdout.isTTY) return value; + return `\u001b[${code}m${value}\u001b[0m`; +} + +function renderBox(content: string, title: string, borderCode = 36): string { + const lines = [` ${title} `, ...content.split('\n').map(line => ` ${line}`)]; + const width = Math.max(...lines.map(line => stripAnsi(line).length)) + 1; + const border = color(`╭${'─'.repeat(width)}╮`, borderCode); + const bottom = color(`╰${'─'.repeat(width)}╯`, borderCode); + return [ + border, + ...lines.map(line => `${color('│', borderCode)}${line}${' '.repeat(Math.max(0, width - stripAnsi(line).length))}${color('│', borderCode)}`), + bottom, + ].join('\n'); +} + +function stripAnsi(value: string): string { + return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''); +} diff --git a/src/data/repository/__tests__/issue_type_repository.test.ts b/src/data/repository/__tests__/issue_type_repository.test.ts index 53013c79..14d2f13f 100644 --- a/src/data/repository/__tests__/issue_type_repository.test.ts +++ b/src/data/repository/__tests__/issue_type_repository.test.ts @@ -54,14 +54,14 @@ describe('IssueTypeRepository', () => { }); await expect(new IssueTypeRepository(new OctokitGraphqlTransportClientAdapter()).listIssueTypes('owner', 'token')) - .rejects.toThrow('no devolvió cursor'); + .rejects.toThrow('did not return a cursor'); }); it('fails clearly when the organization does not exist', async () => { mockGraphql.mockResolvedValue({ organization: null }); await expect(new IssueTypeRepository(new OctokitGraphqlTransportClientAdapter()).listIssueTypes('missing', 'token')) - .rejects.toThrow('No se pudo obtener la organización missing'); + .rejects.toThrow('Could not resolve the organization missing'); }); it('creates an issue type after resolving the organization id', async () => { @@ -86,7 +86,7 @@ describe('IssueTypeRepository', () => { await expect(new IssueTypeRepository(new OctokitGraphqlTransportClientAdapter()).createIssueType( 'missing', 'Bug', 'description', 'ff0000', 'token', - )).rejects.toThrow('No se pudo obtener la organización missing'); + )).rejects.toThrow('Could not resolve the organization missing'); }); it('does not create an issue type that already exists', async () => { @@ -137,7 +137,7 @@ describe('IssueTypeRepository', () => { await expect(new IssueTypeRepository(new OctokitGraphqlTransportClientAdapter()).ensureIssueTypes( 'owner', issueTypes, 'token', )).resolves.toMatchObject({ created: 0, existing: 0, errors: expect.arrayContaining([ - expect.stringContaining('Error creando tipo de Issue "task"'), + expect.stringContaining('Error creating Issue type "task"'), ]) }); }); diff --git a/src/data/repository/__tests__/repository_variables_repository.test.ts b/src/data/repository/__tests__/repository_variables_repository.test.ts new file mode 100644 index 00000000..5412e446 --- /dev/null +++ b/src/data/repository/__tests__/repository_variables_repository.test.ts @@ -0,0 +1,63 @@ +import { RepositoryVariablesRepository } from '../repository_variables_repository'; +import { randomBytes } from 'node:crypto'; + +describe('RepositoryVariablesRepository', () => { + it('creates missing variables and updates existing variables', async () => { + const listRepoVariables = jest.fn().mockResolvedValue({ data: { variables: [{ name: 'EXISTING' }] } }); + const createRepoVariable = jest.fn().mockResolvedValue(undefined); + const updateRepoVariable = jest.fn().mockResolvedValue(undefined); + const client = { rest: { actions: { listRepoVariables, createRepoVariable, updateRepoVariable } } }; + const repository = new RepositoryVariablesRepository({ getClient: jest.fn(() => client) }); + + const result = await repository.upsert('owner', 'repo', 'token', [ + { name: 'EXISTING', value: 'updated' }, + { name: 'NEW', value: 'created' }, + ]); + + expect(result).toEqual({ created: 1, updated: 1, errors: [] }); + expect(updateRepoVariable).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', name: 'EXISTING', value: 'updated' }); + expect(createRepoVariable).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', name: 'NEW', value: 'created' }); + }); + + it('continues and reports an individual variable failure', async () => { + const listRepoVariables = jest.fn().mockResolvedValue({ data: { variables: [] } }); + const createRepoVariable = jest.fn() + .mockRejectedValueOnce(new Error('forbidden')) + .mockResolvedValueOnce(undefined); + const client = { rest: { actions: { listRepoVariables, createRepoVariable, updateRepoVariable: jest.fn() } } }; + const repository = new RepositoryVariablesRepository({ getClient: jest.fn(() => client) }); + + const result = await repository.upsert('owner', 'repo', 'token', [ + { name: 'FIRST', value: 'one' }, + { name: 'SECOND', value: 'two' }, + ]); + + expect(result.created).toBe(1); + expect(result.errors).toEqual(['Error configuring repository Variable FIRST: forbidden']); + }); + + it('lists repository secret names without requesting their values', async () => { + const listRepoSecrets = jest.fn().mockResolvedValue({ data: { secrets: [{ name: 'PAT' }, { name: 'OPENAI_API_KEY' }] } }); + const client = { rest: { actions: { listRepoVariables: jest.fn(), createRepoVariable: jest.fn(), updateRepoVariable: jest.fn() }, secrets: { + listRepoSecrets, getRepoPublicKey: jest.fn(), createOrUpdateRepoSecret: jest.fn(), + } } }; + const repository = new RepositoryVariablesRepository({ getClient: jest.fn(() => client) }); + await expect(repository.list('owner', 'repo', 'token')).resolves.toEqual(['PAT', 'OPENAI_API_KEY']); + expect(listRepoSecrets).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', per_page: 100 }); + }); + + it('encrypts and upserts secret values using the repository public key', async () => { + const listRepoSecrets = jest.fn().mockResolvedValue({ data: { secrets: [{ name: 'PAT' }] } }); + const createOrUpdateRepoSecret = jest.fn().mockResolvedValue(undefined); + const client = { rest: { actions: { listRepoVariables: jest.fn(), createRepoVariable: jest.fn(), updateRepoVariable: jest.fn() }, secrets: { + listRepoSecrets, getRepoPublicKey: jest.fn().mockResolvedValue({ data: { key_id: 'key-id', key: randomBytes(32).toString('base64') } }), createOrUpdateRepoSecret, + } } }; + const repository = new RepositoryVariablesRepository({ getClient: jest.fn(() => client) }); + const result = await repository.upsertSecrets('owner', 'repo', 'token', [{ name: 'PAT', value: 'workflow-token' }, { name: 'OPENAI_API_KEY', value: 'api-key' }]); + expect(result).toEqual({ created: 1, updated: 1, skipped: 0, errors: [] }); + expect(createOrUpdateRepoSecret).toHaveBeenCalledTimes(2); + const payload = createOrUpdateRepoSecret.mock.calls[0][0]; + expect(payload).toMatchObject({ owner: 'owner', repo: 'repo', secret_name: 'PAT', key_id: 'key-id' }); + expect(payload.encrypted_value).not.toContain('workflow-token'); + }); +}); diff --git a/src/data/repository/issue/issue_type_ensure_workflow.ts b/src/data/repository/issue/issue_type_ensure_workflow.ts index 5439eaf8..a4de1e2e 100644 --- a/src/data/repository/issue/issue_type_ensure_workflow.ts +++ b/src/data/repository/issue/issue_type_ensure_workflow.ts @@ -63,7 +63,7 @@ async function ensureConfiguredIssueTypeSafely( } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); logError(`Error ensuring issue type "${configured.name}": ${error}`); - return { kind: 'error', message: `Error creando tipo de Issue "${configured.name}": ${message}` }; + return { kind: 'error', message: `Error creating Issue type "${configured.name}": ${message}` }; } } diff --git a/src/data/repository/issue/issue_type_queries.ts b/src/data/repository/issue/issue_type_queries.ts index fd0cac7a..2b593957 100644 --- a/src/data/repository/issue/issue_type_queries.ts +++ b/src/data/repository/issue/issue_type_queries.ts @@ -46,16 +46,16 @@ export async function listIssueTypes( for (let page = 1; page <= 100; page += 1) { const response: IssueTypePage = await client.graphql(ISSUE_TYPES_QUERY, { owner, after: cursor }); const organization: IssueTypePage["organization"] = response.organization; - if (!organization) throw new Error(`No se pudo obtener la organización ${owner}`); + if (!organization) throw new Error(`Could not resolve the organization ${owner}`); issueTypes.push(...organization.issueTypes.nodes); const pageInfo: NonNullable["issueTypes"]["pageInfo"] = organization.issueTypes.pageInfo; if (!pageInfo?.hasNextPage) return issueTypes; if (!pageInfo.endCursor) { - throw new Error(`La paginación de tipos de Issue no devolvió cursor en la página ${page}.`); + throw new Error(`Issue type pagination did not return a cursor on page ${page}.`); } cursor = pageInfo.endCursor; } - throw new Error("La paginación de tipos de Issue superó 100 páginas."); + throw new Error('Issue type pagination exceeded 100 pages.'); } export async function createIssueType( @@ -69,7 +69,7 @@ export async function createIssueType( ORGANIZATION_ID_QUERY, { owner }, ); - if (!response.organization) throw new Error(`No se pudo obtener la organización ${owner}`); + if (!response.organization) throw new Error(`Could not resolve the organization ${owner}`); const result = await client.graphql<{ createIssueType: { issueType: { id: string } } }>( CREATE_ISSUE_TYPE_MUTATION, diff --git a/src/data/repository/repository_variables_repository.ts b/src/data/repository/repository_variables_repository.ts new file mode 100644 index 00000000..e769fbff --- /dev/null +++ b/src/data/repository/repository_variables_repository.ts @@ -0,0 +1,108 @@ +import type { SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialValue } from '../../domain/setup'; +import type { GithubClientPort } from '../../infrastructure/github/ports/github_client_provider_port'; +import type { GithubRepositoryVariablesClient } from '../../infrastructure/github/ports/github_repository_variables_protocol'; +import nacl from 'tweetnacl'; +import { createHash } from 'node:crypto'; + +export class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort { + constructor(private readonly githubClient: GithubClientPort) {} + + async list(owner: string, repository: string, token: string): Promise { + const client = this.githubClient.getClient(token); + if (!client.rest.secrets) throw new Error('GitHub repository Secret API is unavailable.'); + const response = await client.rest.secrets.listRepoSecrets({ owner, repo: repository, per_page: 100 }); + return response.data.secrets.map(secret => secret.name); + } + + async listVariables(owner: string, repository: string, token: string): Promise { + const client = this.githubClient.getClient(token); + const response = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); + return response.data.variables.map(variable => ({ name: variable.name, value: variable.value })); + } + + async upsertSecrets( + owner: string, + repository: string, + token: string, + credentials: readonly SetupCredentialValue[], + ): Promise<{ created: number; updated: number; skipped: number; errors: string[] }> { + const client = this.githubClient.getClient(token); + if (!client.rest.secrets) throw new Error('GitHub repository Secret API is unavailable.'); + const existing = new Set(await this.list(owner, repository, token)); + const publicKey = await client.rest.secrets.getRepoPublicKey({ owner, repo: repository }); + let created = 0; + let updated = 0; + const skipped = 0; + const errors: string[] = []; + for (const credential of credentials) { + try { + await client.rest.secrets.createOrUpdateRepoSecret({ + owner, + repo: repository, + secret_name: credential.name, + encrypted_value: encryptSecret(credential.value, publicKey.data.key), + key_id: publicKey.data.key_id, + }); + if (existing.has(credential.name)) updated += 1; + else created += 1; + } catch (error) { + errors.push(`Error configuring repository Secret ${credential.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, skipped, errors }; + } + + /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ + async upsert( + owner: string, + repository: string, + token: string, + variables: readonly { name: string; value: string }[], + ): Promise<{ created: number; updated: number; errors: string[] }> { + return this.upsertVariables(owner, repository, token, variables); + } + + private async upsertVariables( + owner: string, + repository: string, + token: string, + variables: readonly { name: string; value: string }[], + ): Promise<{ created: number; updated: number; errors: string[] }> { + const client = this.githubClient.getClient(token); + const existing = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); + const existingValues = new Map(existing.data.variables.map(variable => [variable.name, variable.value])); + let created = 0; + let updated = 0; + const errors: string[] = []; + + for (const variable of variables) { + try { + if (existingValues.has(variable.name)) { + if (existingValues.get(variable.name) === variable.value) continue; + await client.rest.actions.updateRepoVariable({ owner, repo: repository, name: variable.name, value: variable.value }); + updated += 1; + } else { + await client.rest.actions.createRepoVariable({ owner, repo: repository, name: variable.name, value: variable.value }); + created += 1; + } + } catch (error) { + errors.push(`Error configuring repository Variable ${variable.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, errors }; + } +} + +/** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ +export function encryptSecret(value: string, base64PublicKey: string): string { + const publicKey = Buffer.from(base64PublicKey, 'base64'); + if (publicKey.length !== nacl.box.publicKeyLength) throw new Error('GitHub returned an invalid repository public key.'); + const keyPair = nacl.box.keyPair(); + const nonce = createHash('blake2b512') + .update(Buffer.concat([Buffer.from(keyPair.publicKey), publicKey])) + .digest() + .subarray(0, nacl.box.nonceLength); + const ciphertext = nacl.box(Buffer.from(value, 'utf8'), nonce, publicKey, keyPair.secretKey); + return Buffer.from(Buffer.concat([Buffer.from(keyPair.publicKey), Buffer.from(ciphertext)])).toString('base64'); +} diff --git a/src/domain/setup.ts b/src/domain/setup.ts new file mode 100644 index 00000000..1524cf34 --- /dev/null +++ b/src/domain/setup.ts @@ -0,0 +1,139 @@ +import type { AgentProvider, AgentTask } from './agent'; + +export type SetupFeature = + | 'issues' + | 'pullRequests' + | 'commits' + | 'issueComments' + | 'pullRequestComments' + | 'release' + | 'hotfix' + | 'agentProvisioning' + | 'credentialHealth' + | 'issueTemplates' + | 'pullRequestTemplate'; + +export interface SetupFeatures { + [feature: string]: boolean; +} + +export interface SetupAgentRoleConfiguration { + provider: AgentProvider; + modelProvider: string; + model: string; + effort?: string; +} + +export type SetupAgentConfiguration = Record; + +export interface SetupRepositoryConfiguration { + mainBranch: string; + developmentBranch: string; + featureTree: string; + bugfixTree: string; + hotfixTree: string; + releaseTree: string; + docsTree: string; + choreTree: string; + branchManagementAlways: boolean; + reopenIssueOnPush: boolean; + desiredAssigneesCount: number; + desiredReviewersCount: number; + mergeTimeout: number; + issueLocale: string; + pullRequestLocale: string; + commitPrefixTransforms: string; +} + +export interface SetupAiConfiguration { + pullRequestDescription: boolean; + ignoreFiles: string; + membersOnly: boolean; + includeReasoning: boolean; + bugbotSeverity: 'info' | 'low' | 'medium' | 'high'; + bugbotCommentLimit: number; + bugbotFixVerifyCommands: string; + provisioningMode: 'auto' | 'always' | 'disabled'; +} + +export interface SetupProjectConfiguration { + ids: string; + issueCreatedColumn: string; + pullRequestCreatedColumn: string; + issueInProgressColumn: string; + pullRequestInProgressColumn: string; +} + +export interface SetupConfiguration { + features: SetupFeatures; + agents: SetupAgentConfiguration; + repository: SetupRepositoryConfiguration; + ai: SetupAiConfiguration; + projects: SetupProjectConfiguration; + createInitialTag: boolean; + manageRepositoryVariables: boolean; + /** Whether setup should provision repository secrets after validating them. */ + manageRepositorySecrets: boolean; + /** Extra non-secret action inputs accepted by config files for advanced use cases. */ + actionInputs: Record; +} + +export type SetupCredentialKind = 'workflowPat' | 'apiKey'; +export type SetupCredentialStatus = 'valid' | 'invalid' | 'missing' | 'unverifiable' | 'not_required'; + +/** A credential requirement is metadata only; never put a secret value in this object. */ +export interface SetupCredentialRequirement { + name: string; + kind: SetupCredentialKind; + description: string; + provider?: string; + model?: string; +} + +export interface SetupCredentialCheck { + name: string; + status: SetupCredentialStatus; + message: string; + account?: string; +} + +export interface SetupCredentialValue { + name: string; + value: string; +} + +export type SetupCredentialDecision = 'keep' | 'replace' | 'skip'; + +export interface SetupCredentialCollection { + workflowPat?: SetupCredentialValue; + apiKeys: SetupCredentialValue[]; +} + +export interface SetupWorkflowComparison { + file: string; + destination: string; + status: 'missing' | 'unchanged' | 'changed' | 'unmanaged'; +} + +export type DoctorCheckStatus = 'pass' | 'warn' | 'fail'; +export interface DoctorCheck { + area: string; + status: DoctorCheckStatus; + message: string; +} + +export interface SetupVariable { + name: string; + value: string; +} + +export interface SetupPlan { + configuration: SetupConfiguration; + workflowFiles: string[]; + issueTemplateFiles: string[]; + selectedFiles: string[]; + variables: SetupVariable[]; + requiredSecrets: string[]; + credentialRequirements: SetupCredentialRequirement[]; + warnings: string[]; +} diff --git a/src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts b/src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts new file mode 100644 index 00000000..55f02260 --- /dev/null +++ b/src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts @@ -0,0 +1,72 @@ +import { SetupCredentialValidationAdapter } from '../setup_credential_validation_adapter'; + +function response(body: unknown, ok = true, status = 200): Response { + return { ok, status, json: jest.fn().mockResolvedValue(body) } as unknown as Response; +} + +describe('SetupCredentialValidationAdapter', () => { + it('validates setup identity and repository access without logging the token', async () => { + const fetcher = jest.fn() + .mockResolvedValueOnce(response({ login: 'operator' })) + .mockResolvedValueOnce(response({ full_name: 'repo' })); + const check = await new SetupCredentialValidationAdapter({ fetcher }).validateSetupPat('owner', 'repo', 'secret-token'); + + expect(check).toMatchObject({ name: 'SETUP_PAT', status: 'valid', account: 'operator' }); + expect(fetcher).toHaveBeenNthCalledWith(1, 'https://api.github.com/user', expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer secret-token' }) })); + expect(check.message).not.toContain('secret-token'); + }); + + it('classifies provider authentication failures as invalid', async () => { + const fetcher = jest.fn().mockResolvedValue(response({}, false, 401)); + const check = await new SetupCredentialValidationAdapter({ fetcher }).validateCredential({ + name: 'OPENAI_API_KEY', kind: 'apiKey', description: 'OpenAI', provider: 'openai', model: 'gpt-5.6-luna', + }, 'secret-key'); + + expect(check).toEqual({ name: 'OPENAI_API_KEY', status: 'invalid', message: 'Provider rejected the credential (HTTP 401).' }); + }); + + it('validates provider metadata with the provider-specific auth scheme', async () => { + const fetcher = jest.fn().mockResolvedValue(response({ data: [{ id: 'gpt-5.6-luna' }] })); + const check = await new SetupCredentialValidationAdapter({ fetcher }).validateCredential({ + name: 'OPENAI_API_KEY', kind: 'apiKey', description: 'OpenAI', provider: 'openai', model: 'gpt-5.6-luna', + }, 'secret-key'); + + expect(check.status).toBe('valid'); + expect(fetcher).toHaveBeenCalledWith('https://api.openai.com/v1/models', expect.objectContaining({ + headers: expect.objectContaining({ Authorization: 'Bearer secret-key' }), + })); + }); + + it('reports provider keys without a safe endpoint as unverifiable', async () => { + const check = await new SetupCredentialValidationAdapter({ fetcher: jest.fn() }).validateCredential({ + name: 'CUSTOM_API_KEY', kind: 'apiKey', description: 'Custom provider', provider: 'custom', + }, 'secret-key'); + expect(check.status).toBe('unverifiable'); + }); + + it('classifies transient provider failures as unverifiable', async () => { + const fetcher = jest.fn().mockResolvedValue(response({}, false, 503)); + const check = await new SetupCredentialValidationAdapter({ fetcher }).validateCredential({ + name: 'OPENAI_API_KEY', kind: 'apiKey', description: 'OpenAI', provider: 'openai', + }, 'secret-key'); + expect(check.status).toBe('unverifiable'); + }); + + it('checks Google keys through the query parameter and accepts model names with the resource prefix', async () => { + const fetcher = jest.fn().mockResolvedValue(response({ models: [{ name: 'models/gemini-2.5-pro' }] })); + const check = await new SetupCredentialValidationAdapter({ fetcher }).validateCredential({ + name: 'GOOGLE_API_KEY', kind: 'apiKey', description: 'Google', provider: 'google', model: 'gemini-2.5-pro', + }, 'secret-key'); + expect(check.status).toBe('valid'); + expect(fetcher.mock.calls[0][0]).toContain('key=secret-key'); + }); + + it('rejects a valid key when the selected model is not available', async () => { + const fetcher = jest.fn().mockResolvedValue(response({ data: [{ id: 'other-model' }] })); + const check = await new SetupCredentialValidationAdapter({ fetcher }).validateCredential({ + name: 'OPENROUTER_API_KEY', kind: 'apiKey', description: 'OpenRouter', provider: 'openrouter', model: 'requested-model', + }, 'secret-key'); + expect(check.status).toBe('invalid'); + expect(check.message).toContain('not available'); + }); +}); diff --git a/src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts b/src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts new file mode 100644 index 00000000..c698fc05 --- /dev/null +++ b/src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts @@ -0,0 +1,104 @@ +import { SetupRemoteCredentialHealthAdapter } from '../setup_remote_credential_health_adapter'; + +const requirements = [{ name: 'PAT', kind: 'workflowPat' as const, description: 'workflow PAT' }, { name: 'OPENAI_API_KEY', kind: 'apiKey' as const, description: 'OpenAI' }]; + +function client(overrides: Record = {}) { + return { + rest: { + actions: { + getWorkflow: jest.fn().mockResolvedValue({}), + createWorkflowDispatch: jest.fn().mockResolvedValue(undefined), + listWorkflowRuns: jest.fn().mockResolvedValue({ data: { workflow_runs: [{ id: 1, status: 'completed', conclusion: 'success', created_at: new Date().toISOString() }] } }), + getWorkflowRun: jest.fn(), + listJobsForWorkflowRun: jest.fn().mockResolvedValue({ data: { jobs: [ + { name: 'Verify PAT', status: 'completed', conclusion: 'success' }, + { name: 'Verify OPENAI_API_KEY', status: 'completed', conclusion: 'success' }, + ] } }), + ...overrides, + }, + }, + repos: { + get: jest.fn(), getContent: jest.fn(), createOrUpdateFileContents: jest.fn(), deleteFile: jest.fn(), + }, + }; +} + +describe('SetupRemoteCredentialHealthAdapter', () => { + it('dispatches the health workflow and maps a successful run to valid checks', async () => { + const github = client(); + const adapter = new SetupRemoteCredentialHealthAdapter({ getClient: jest.fn(() => github) }, { waitMs: 0, pollMs: 0 }); + const checks = await adapter.validateExisting('owner', 'repo', 'token', 'main', requirements); + + expect(checks).toEqual([ + { name: 'PAT', status: 'valid', message: 'Remote credential health check passed.' }, + { name: 'OPENAI_API_KEY', status: 'valid', message: 'Remote credential health check passed.' }, + ]); + expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith(expect.objectContaining({ + workflow_id: 'copilot_credential_health.yml', ref: 'main', inputs: { check_pat: 'true', check_openai: 'true' }, + })); + }); + + it('returns undefined when the health workflow has not been installed', async () => { + const error = Object.assign(new Error('not found'), { status: 404 }); + const github = client({ getWorkflow: jest.fn().mockRejectedValue(error) }); + const checks = await new SetupRemoteCredentialHealthAdapter({ getClient: jest.fn(() => github) }).validateExisting('owner', 'repo', 'token', 'main', requirements); + expect(checks).toBeUndefined(); + expect(github.rest.actions.createWorkflowDispatch).not.toHaveBeenCalled(); + }); + + it('maps each credential from its own remote job result', async () => { + const github = client({ + listWorkflowRuns: jest.fn().mockResolvedValue({ data: { workflow_runs: [{ id: 2, status: 'completed', conclusion: 'failure' }] } }), + listJobsForWorkflowRun: jest.fn().mockResolvedValue({ data: { jobs: [ + { name: 'Verify PAT', status: 'completed', conclusion: 'success' }, + { name: 'Verify OPENAI_API_KEY', status: 'completed', conclusion: 'failure' }, + ] } }), + }); + const checks = await new SetupRemoteCredentialHealthAdapter({ getClient: jest.fn(() => github) }, { waitMs: 0 }).validateExisting('owner', 'repo', 'token', 'main', requirements); + expect(checks).toEqual([ + { name: 'PAT', status: 'valid', message: 'Remote credential health check passed.' }, + { name: 'OPENAI_API_KEY', status: 'invalid', message: 'Remote credential health check failed (failure).' }, + ]); + }); + + it('polls a queued run until it completes', async () => { + const github = client({ + listWorkflowRuns: jest.fn().mockResolvedValue({ data: { workflow_runs: [{ id: 3, status: 'in_progress', conclusion: null }] } }), + getWorkflowRun: jest.fn().mockResolvedValue({ data: { id: 3, status: 'completed', conclusion: 'success' } }), + }); + const sleep = jest.fn().mockResolvedValue(undefined); + const checks = await new SetupRemoteCredentialHealthAdapter({ getClient: jest.fn(() => github) }, { waitMs: 100, pollMs: 0, sleep }).validateExisting('owner', 'repo', 'token', 'main', requirements); + expect(checks?.[0].status).toBe('valid'); + expect(sleep).toHaveBeenCalled(); + }); + + it('reports an unverifiable result when GitHub does not return a run', async () => { + const github = client({ listWorkflowRuns: jest.fn().mockResolvedValue({ data: { workflow_runs: [] } }) }); + const checks = await new SetupRemoteCredentialHealthAdapter({ getClient: jest.fn(() => github) }, { waitMs: 0, pollMs: 0 }).validateExisting('owner', 'repo', 'token', 'main', requirements); + expect(checks?.every(check => check.status === 'unverifiable')).toBe(true); + }); + + it('does not claim an unsupported credential passed just because the workflow passed', async () => { + const github = client({ + listJobsForWorkflowRun: jest.fn().mockResolvedValue({ data: { jobs: [] } }), + }); + const checks = await new SetupRemoteCredentialHealthAdapter({ getClient: jest.fn(() => github) }, { waitMs: 0 }).validateExisting( + 'owner', 'repo', 'token', 'main', [...requirements, { name: 'MISTRAL_API_KEY', kind: 'apiKey' as const, description: 'Mistral' }], + ); + expect(checks?.find(check => check.name === 'MISTRAL_API_KEY')).toEqual({ + name: 'MISTRAL_API_KEY', status: 'unverifiable', message: 'No remote health check is implemented for this provider.', + }); + }); + + it('temporarily installs and removes the health workflow when setup explicitly enables bootstrap', async () => { + const error = Object.assign(new Error('not found'), { status: 404 }); + const github = client({ getWorkflow: jest.fn().mockRejectedValue(error) }); + github.repos.getContent.mockResolvedValue({ data: { sha: 'temporary-sha' } }); + const checks = await new SetupRemoteCredentialHealthAdapter({ getClient: jest.fn(() => github) }, { + bootstrapWhenMissing: true, workflowContent: 'name: health', waitMs: 0, pollMs: 0, + }).validateExisting('owner', 'repo', 'token', 'main', requirements); + expect(checks?.every(check => check.status === 'valid')).toBe(true); + expect(github.repos.createOrUpdateFileContents).toHaveBeenCalledWith(expect.objectContaining({ branch: 'main' })); + expect(github.repos.deleteFile).toHaveBeenCalledWith(expect.objectContaining({ sha: 'temporary-sha', branch: 'main' })); + }); +}); diff --git a/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts b/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts index 6c550db9..d59fa30e 100644 --- a/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts @@ -39,7 +39,7 @@ describe('initial setup composition root', () => { mockIssueLabelProvisioningRepository.mockClear(); }); - it('injects one initial-label provisioning capability', () => { + it('injects one initial-label provisioning capability and repository-variable provisioning', () => { const composed = createInitialSetupCompositionRoot(); expect(composed).toEqual({ taskId: 'composed' }); @@ -50,7 +50,7 @@ describe('initial setup composition root', () => { ); expect(mockComposeInitialSetupUseCase).toHaveBeenCalledTimes(1); const dependencies = mockComposeInitialSetupUseCase.mock.calls[0]; - expect(dependencies).toHaveLength(7); + expect(dependencies).toHaveLength(9); expect(dependencies[1]).toBe(mockLabelProvisioning); }); }); diff --git a/src/infrastructure/composition/github_identity_client_factory.ts b/src/infrastructure/composition/github_identity_client_factory.ts index 20aff4ac..8fb001cd 100644 --- a/src/infrastructure/composition/github_identity_client_factory.ts +++ b/src/infrastructure/composition/github_identity_client_factory.ts @@ -1,4 +1,6 @@ import { OctokitAuthenticatedUserClientAdapter, OctokitActorAuthorizationClientAdapter, OctokitOrganizationMembersClientAdapter } from "../github/octokit_identity_adapters"; +import { OctokitRepositoryVariablesClientAdapter } from '../github/octokit_repository_variables_adapter'; export const createAuthenticatedUserClient = () => new OctokitAuthenticatedUserClientAdapter(); export const createActorAuthorizationClient = () => new OctokitActorAuthorizationClientAdapter(); export const createOrganizationMembersClient = () => new OctokitOrganizationMembersClientAdapter(); +export const createRepositoryVariablesClient = () => new OctokitRepositoryVariablesClientAdapter(); diff --git a/src/infrastructure/composition/initial_setup_composition_root.ts b/src/infrastructure/composition/initial_setup_composition_root.ts index b76b6131..1002b9b2 100644 --- a/src/infrastructure/composition/initial_setup_composition_root.ts +++ b/src/infrastructure/composition/initial_setup_composition_root.ts @@ -12,12 +12,15 @@ import { RepositoryTagRepository } from "../../data/repository/release/repositor import { GitCliRepository } from "../../data/repository/git_cli_repository"; import { composeInitialSetupUseCase } from "./initial_setup_use_case_composition"; import { SetupWorkspaceAdapter } from "../setup_workspace_adapter"; +import { RepositoryVariablesRepository } from '../../data/repository/repository_variables_repository'; +import { createRepositoryVariablesClient } from './github_identity_client_factory'; export function createInitialSetupCompositionRoot(): InitialSetupUseCase { const labelProvisioning = new IssueLabelProvisioningRepository( createIssueLabelProvisioningClient(), ); + const repositoryConfiguration = new RepositoryVariablesRepository(createRepositoryVariablesClient()); return composeInitialSetupUseCase( new AuthenticatedUserRepository(createAuthenticatedUserClient()), labelProvisioning, @@ -26,5 +29,7 @@ export function createInitialSetupCompositionRoot(): InitialSetupUseCase { new RepositoryDefaultBranchRepository(createReleaseClient()), new RepositoryTagRepository(createReleaseClient()), new SetupWorkspaceAdapter(), + repositoryConfiguration, + repositoryConfiguration, ); } diff --git a/src/infrastructure/composition/setup_credentials_composition_root.ts b/src/infrastructure/composition/setup_credentials_composition_root.ts new file mode 100644 index 00000000..0312ba36 --- /dev/null +++ b/src/infrastructure/composition/setup_credentials_composition_root.ts @@ -0,0 +1,17 @@ +import { SetupCredentialsUseCase } from '../../application/usecases/setup/setup_credentials_use_case'; +import type { SetupCredentialPromptPort } from '../../application/ports/setup_wizard_ports'; +import { SetupCredentialValidationAdapter } from '../setup_credential_validation_adapter'; +import { RepositoryVariablesRepository } from '../../data/repository/repository_variables_repository'; +import { createRepositoryVariablesClient } from './github_identity_client_factory'; +import { SetupRemoteCredentialHealthAdapter } from '../setup_remote_credential_health_adapter'; +import { OctokitCredentialHealthClientAdapter } from '../github/octokit_credential_health_adapter'; + +export function createSetupCredentialsUseCase(prompt: SetupCredentialPromptPort): SetupCredentialsUseCase { + const repositoryConfiguration = new RepositoryVariablesRepository(createRepositoryVariablesClient()); + return new SetupCredentialsUseCase( + prompt, + new SetupCredentialValidationAdapter(), + repositoryConfiguration, + new SetupRemoteCredentialHealthAdapter(new OctokitCredentialHealthClientAdapter(), { bootstrapWhenMissing: true }), + ); +} diff --git a/src/infrastructure/composition/setup_doctor_composition_root.ts b/src/infrastructure/composition/setup_doctor_composition_root.ts new file mode 100644 index 00000000..2248445b --- /dev/null +++ b/src/infrastructure/composition/setup_doctor_composition_root.ts @@ -0,0 +1,20 @@ +import { SetupDoctorUseCase } from '../../application/usecases/setup/doctor_use_case'; +import type { DoctorOutputPort } from '../../application/ports/setup_wizard_ports'; +import { SetupCredentialValidationAdapter } from '../setup_credential_validation_adapter'; +import { RepositoryVariablesRepository } from '../../data/repository/repository_variables_repository'; +import { createRepositoryVariablesClient } from './github_identity_client_factory'; +import { SetupWorkspaceAdapter } from '../setup_workspace_adapter'; +import { SetupRemoteCredentialHealthAdapter } from '../setup_remote_credential_health_adapter'; +import { OctokitCredentialHealthClientAdapter } from '../github/octokit_credential_health_adapter'; + +export function createSetupDoctorUseCase(output: DoctorOutputPort): SetupDoctorUseCase { + const repositoryConfiguration = new RepositoryVariablesRepository(createRepositoryVariablesClient()); + return new SetupDoctorUseCase( + new SetupCredentialValidationAdapter(), + repositoryConfiguration, + repositoryConfiguration, + new SetupWorkspaceAdapter(), + output, + new SetupRemoteCredentialHealthAdapter(new OctokitCredentialHealthClientAdapter()), + ); +} diff --git a/src/infrastructure/github/octokit_credential_health_adapter.ts b/src/infrastructure/github/octokit_credential_health_adapter.ts new file mode 100644 index 00000000..a0c700f3 --- /dev/null +++ b/src/infrastructure/github/octokit_credential_health_adapter.ts @@ -0,0 +1,9 @@ +import { getOctokitClient } from './octokit_client_resolver'; +import type { GithubClientPort } from './ports/github_client_provider_port'; +import type { GithubCredentialHealthClient } from './ports/github_credential_health_protocol'; + +export class OctokitCredentialHealthClientAdapter implements GithubClientPort { + getClient(token: string): GithubCredentialHealthClient { + return getOctokitClient(token); + } +} diff --git a/src/infrastructure/github/octokit_repository_variables_adapter.ts b/src/infrastructure/github/octokit_repository_variables_adapter.ts new file mode 100644 index 00000000..4f85760c --- /dev/null +++ b/src/infrastructure/github/octokit_repository_variables_adapter.ts @@ -0,0 +1,9 @@ +import { getOctokitClient } from './octokit_client_resolver'; +import type { GithubClientPort } from './ports/github_client_provider_port'; +import type { GithubRepositoryVariablesClient } from './ports/github_repository_variables_protocol'; + +export class OctokitRepositoryVariablesClientAdapter implements GithubClientPort { + getClient(token: string): GithubRepositoryVariablesClient { + return getOctokitClient(token); + } +} diff --git a/src/infrastructure/github/ports/github_credential_health_protocol.ts b/src/infrastructure/github/ports/github_credential_health_protocol.ts new file mode 100644 index 00000000..7c2ef5ff --- /dev/null +++ b/src/infrastructure/github/ports/github_credential_health_protocol.ts @@ -0,0 +1,30 @@ +export interface GithubCredentialHealthClient { + rest: { + actions: { + createWorkflowDispatch(parameters: Record): Promise; + listWorkflowRuns(parameters: Record): Promise<{ data: { workflow_runs: GithubWorkflowRun[] } }>; + getWorkflowRun(parameters: Record): Promise<{ data: GithubWorkflowRun }>; + listJobsForWorkflowRun(parameters: Record): Promise<{ data: { jobs: GithubWorkflowJob[] } }>; + getWorkflow(parameters: Record): Promise; + }; + }; + repos: { + get(parameters: Record): Promise<{ data: { default_branch?: string } }>; + getContent(parameters: Record): Promise<{ data: { sha?: string } }>; + createOrUpdateFileContents(parameters: Record): Promise<{ data?: { content?: { sha?: string } } }>; + deleteFile(parameters: Record): Promise; + }; +} + +export interface GithubWorkflowRun { + id: number; + status?: string | null; + conclusion?: string | null; + created_at?: string; +} + +export interface GithubWorkflowJob { + name: string; + status?: string | null; + conclusion?: string | null; +} diff --git a/src/infrastructure/github/ports/github_repository_variables_protocol.ts b/src/infrastructure/github/ports/github_repository_variables_protocol.ts new file mode 100644 index 00000000..677ba3f4 --- /dev/null +++ b/src/infrastructure/github/ports/github_repository_variables_protocol.ts @@ -0,0 +1,25 @@ +export interface GithubRepositoryVariable { + name: string; + value?: string; +} + +export interface GithubRepositoryVariablesClient { + rest: { + actions: { + listRepoVariables(parameters: Record): Promise<{ data: { variables: GithubRepositoryVariable[] } }>; + createRepoVariable(parameters: Record): Promise; + updateRepoVariable(parameters: Record): Promise; + }; + secrets?: { + listRepoSecrets(parameters: Record): Promise<{ data: { secrets: GithubRepositorySecret[] } }>; + getRepoPublicKey(parameters: Record): Promise<{ data: { key_id: string; key: string } }>; + createOrUpdateRepoSecret(parameters: Record): Promise; + }; + }; +} + +export interface GithubRepositorySecret { + name: string; + created_at?: string; + updated_at?: string; +} diff --git a/src/infrastructure/setup_credential_validation_adapter.ts b/src/infrastructure/setup_credential_validation_adapter.ts new file mode 100644 index 00000000..1ba2ae49 --- /dev/null +++ b/src/infrastructure/setup_credential_validation_adapter.ts @@ -0,0 +1,134 @@ +import type { + SetupCredentialCheck, + SetupCredentialRequirement, +} from '../domain/setup'; +import type { SetupCredentialValidationPort } from '../application/ports/setup_wizard_ports'; + +export interface SetupCredentialValidationOptions { + fetcher?: typeof fetch; + timeoutMs?: number; +} + +/** + * Performs bounded, metadata-only credential checks. Provider responses are + * intentionally never returned or logged because they can contain account data. + */ +export class SetupCredentialValidationAdapter implements SetupCredentialValidationPort { + private readonly fetcher: typeof fetch; + private readonly timeoutMs: number; + + constructor(options: SetupCredentialValidationOptions = {}) { + this.fetcher = options.fetcher ?? fetch; + this.timeoutMs = options.timeoutMs ?? 10_000; + } + + async validateSetupPat(owner: string, repository: string, token: string): Promise { + try { + const user = await this.requestJson('https://api.github.com/user', { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + }); + const account = typeof user.login === 'string' ? user.login : undefined; + await this.requestJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}`, { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + }); + return { name: 'SETUP_PAT', status: 'valid', message: 'GitHub identity and repository access verified.', account }; + } catch (error) { + return { name: 'SETUP_PAT', status: classifyError(error), message: safeMessage(error) }; + } + } + + async validateCredential(requirement: SetupCredentialRequirement, value: string): Promise { + const endpoint = endpointFor(requirement); + if (!endpoint) { + return { name: requirement.name, status: 'unverifiable', message: 'This provider does not expose a safe metadata-only validation endpoint.' }; + } + try { + const headers: Record = { Accept: 'application/json' }; + const init: RequestInit = { method: 'GET', headers }; + if (endpoint.auth === 'bearer') headers.Authorization = `Bearer ${value}`; + if (endpoint.auth === 'x-api-key') headers['x-api-key'] = value; + if (endpoint.auth === 'query') endpoint.url.searchParams.set('key', value); + if (endpoint.auth === 'basic') headers.Authorization = `Basic ${Buffer.from(`${value}:`).toString('base64')}`; + if (requirement.provider === 'anthropic') headers['anthropic-version'] = '2023-06-01'; + const response = await this.requestJson(endpoint.url.toString(), headers, init); + if (requirement.model && !modelIsAvailable(response, requirement.model, requirement.provider)) { + return { name: requirement.name, status: 'invalid', message: `Credential is valid, but model ${requirement.model} is not available to it.` }; + } + return { name: requirement.name, status: 'valid', message: 'Provider metadata request succeeded.' }; + } catch (error) { + return { name: requirement.name, status: classifyError(error), message: safeMessage(error) }; + } finally { + if (endpoint.auth === 'query') endpoint.url.searchParams.delete('key'); + } + } + + private async requestJson(url: string, headers: Record, init: RequestInit = {}): Promise> { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const response = await this.fetcher(url, { ...init, headers, signal: controller.signal }); + if (!response.ok) throw new CredentialHttpError(response.status); + const body: unknown = await response.json(); + return body && typeof body === 'object' ? body as Record : {}; + } finally { + clearTimeout(timeout); + } + } +} + +interface CredentialEndpoint { + url: URL; + auth: 'bearer' | 'x-api-key' | 'query' | 'basic'; +} + +function endpointFor(requirement: SetupCredentialRequirement): CredentialEndpoint | undefined { + switch (requirement.name) { + case 'OPENAI_API_KEY': + case 'CODEX_ACCESS_TOKEN': + return { url: new URL('https://api.openai.com/v1/models'), auth: 'bearer' }; + case 'ANTHROPIC_API_KEY': + return { url: new URL('https://api.anthropic.com/v1/models'), auth: 'x-api-key' }; + case 'GOOGLE_API_KEY': + return { url: new URL('https://generativelanguage.googleapis.com/v1beta/models'), auth: 'query' }; + case 'OPENROUTER_API_KEY': + return { url: new URL('https://openrouter.ai/api/v1/models'), auth: 'bearer' }; + case 'CURSOR_API_KEY': + return { url: new URL('https://api.cursor.com/analytics/ai-code/changes?startDate=30d&page=1&pageSize=1'), auth: 'basic' }; + case 'OPENCODE_API_KEY': + return { url: new URL('https://opencode.ai/zen/v1/models'), auth: 'bearer' }; + default: + return undefined; + } +} + +function modelIsAvailable(payload: Record, model: string, provider?: string): boolean { + const data = Array.isArray(payload.data) ? payload.data : Array.isArray(payload.models) ? payload.models : []; + if (data.length === 0) return true; + const normalized = model.replace(/^models\//, '').toLowerCase(); + return data.some(item => { + if (!item || typeof item !== 'object') return false; + const candidate = item as Record; + const id = String(candidate.id ?? candidate.name ?? '').replace(/^models\//, '').toLowerCase(); + return id === normalized || (provider === 'google' && id.endsWith(`/${normalized}`)); + }); +} + +class CredentialHttpError extends Error { + constructor(readonly status: number) { + super(`Provider rejected the credential (HTTP ${status}).`); + } +} + +function classifyError(error: unknown): SetupCredentialCheck['status'] { + if (error instanceof CredentialHttpError && (error.status === 401 || error.status === 403)) return 'invalid'; + if (error instanceof CredentialHttpError && error.status >= 400 && error.status < 500) return 'invalid'; + return 'unverifiable'; +} + +function safeMessage(error: unknown): string { + if (error instanceof CredentialHttpError) return error.message; + if (error instanceof DOMException && error.name === 'AbortError') return 'Validation timed out.'; + return 'Provider validation could not be completed. Check network access and try again.'; +} diff --git a/src/infrastructure/setup_remote_credential_health_adapter.ts b/src/infrastructure/setup_remote_credential_health_adapter.ts new file mode 100644 index 00000000..b8e79e9c --- /dev/null +++ b/src/infrastructure/setup_remote_credential_health_adapter.ts @@ -0,0 +1,178 @@ +import { readFileSync } from 'node:fs'; +import * as path from 'node:path'; +import type { + SetupCredentialCheck, + SetupCredentialRequirement, +} from '../domain/setup'; +import type { SetupRemoteCredentialHealthPort } from '../application/ports/setup_wizard_ports'; +import type { GithubClientPort } from './github/ports/github_client_provider_port'; +import type { GithubCredentialHealthClient } from './github/ports/github_credential_health_protocol'; + +const WORKFLOW_ID = 'copilot_credential_health.yml'; +const INPUT_BY_SECRET: Readonly> = { + PAT: 'check_pat', + OPENAI_API_KEY: 'check_openai', + ANTHROPIC_API_KEY: 'check_anthropic', + GOOGLE_API_KEY: 'check_google', + OPENROUTER_API_KEY: 'check_openrouter', + CURSOR_API_KEY: 'check_cursor', + OPENCODE_API_KEY: 'check_opencode', + CODEX_ACCESS_TOKEN: 'check_codex', +}; +const JOB_BY_SECRET: Readonly> = { + PAT: 'Verify PAT', + OPENAI_API_KEY: 'Verify OPENAI_API_KEY', + ANTHROPIC_API_KEY: 'Verify ANTHROPIC_API_KEY', + GOOGLE_API_KEY: 'Verify GOOGLE_API_KEY', + OPENROUTER_API_KEY: 'Verify OPENROUTER_API_KEY', + CURSOR_API_KEY: 'Verify CURSOR_API_KEY', + OPENCODE_API_KEY: 'Verify OPENCODE_API_KEY', + CODEX_ACCESS_TOKEN: 'Verify CODEX_ACCESS_TOKEN', +}; + +export interface CredentialHealthAdapterOptions { + waitMs?: number; + pollMs?: number; + sleep?: (milliseconds: number) => Promise; + bootstrapWhenMissing?: boolean; + workflowContent?: string; +} + +/** Dispatches the repository-owned health workflow; it cannot read or mutate Secret values. */ +export class SetupRemoteCredentialHealthAdapter implements SetupRemoteCredentialHealthPort { + private readonly waitMs: number; + private readonly pollMs: number; + private readonly sleep: (milliseconds: number) => Promise; + private readonly bootstrapWhenMissing: boolean; + private readonly workflowContent: string; + + constructor( + private readonly githubClient: GithubClientPort, + options: CredentialHealthAdapterOptions = {}, + ) { + this.waitMs = options.waitMs ?? 120_000; + this.pollMs = options.pollMs ?? 2_000; + this.sleep = options.sleep ?? (milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))); + this.bootstrapWhenMissing = options.bootstrapWhenMissing ?? false; + this.workflowContent = options.workflowContent ?? readHealthWorkflow(); + } + + async validateExisting( + owner: string, + repository: string, + token: string, + ref: string, + requirements: readonly SetupCredentialRequirement[], + ): Promise { + const client = this.githubClient.getClient(token); + let temporaryWorkflow = false; + try { + await client.rest.actions.getWorkflow({ owner, repo: repository, workflow_id: WORKFLOW_ID }); + } catch (error) { + if (isNotFound(error) && this.bootstrapWhenMissing) { + await this.bootstrapWorkflow(client, owner, repository, ref); + temporaryWorkflow = true; + } else if (isNotFound(error)) return undefined; + else throw error; + } + const inputs: Record = {}; + for (const requirement of requirements) { + const input = INPUT_BY_SECRET[requirement.name]; + if (input) inputs[input] = 'true'; + } + const startedAt = Date.now(); + try { + await client.rest.actions.createWorkflowDispatch({ owner, repo: repository, workflow_id: WORKFLOW_ID, ref, inputs }); + const run = await this.findRun(client, owner, repository, startedAt); + if (!run) return requirements.map(requirement => ({ name: requirement.name, status: 'unverifiable', message: 'Credential health workflow did not produce a run before timeout.' })); + const jobs = await client.rest.actions.listJobsForWorkflowRun({ owner, repo: repository, run_id: run.id, per_page: 100 }); + const jobsByName = new Map(jobs.data.jobs.map(job => [job.name, job])); + return requirements.map(requirement => ({ + name: requirement.name, + status: healthStatus(requirement, jobsByName), + message: healthMessage(requirement, jobsByName), + })); + } finally { + if (temporaryWorkflow) await this.removeTemporaryWorkflow(client, owner, repository, ref); + } + } + + private async bootstrapWorkflow(client: GithubCredentialHealthClient, owner: string, repository: string, ref: string): Promise { + if (!this.workflowContent) throw new Error('Credential health workflow template is unavailable.'); + await client.repos.createOrUpdateFileContents({ + owner, + repo: repository, + path: `.github/workflows/${WORKFLOW_ID}`, + message: 'chore: temporarily validate Copilot credentials', + content: Buffer.from(this.workflowContent, 'utf8').toString('base64'), + branch: ref, + }); + } + + private async removeTemporaryWorkflow(client: GithubCredentialHealthClient, owner: string, repository: string, ref: string): Promise { + const content = await client.repos.getContent({ owner, repo: repository, path: `.github/workflows/${WORKFLOW_ID}`, ref }); + if (!content.data.sha) throw new Error('Could not resolve the temporary health workflow revision for cleanup.'); + await client.repos.deleteFile({ + owner, + repo: repository, + path: `.github/workflows/${WORKFLOW_ID}`, + message: 'chore: remove temporary Copilot credential health workflow', + sha: content.data.sha, + branch: ref, + }); + } + + private async findRun(client: GithubCredentialHealthClient, owner: string, repository: string, startedAt: number): Promise<{ id: number; conclusion?: string | null } | undefined> { + const deadline = Date.now() + this.waitMs; + while (Date.now() <= deadline) { + const response = await client.rest.actions.listWorkflowRuns({ owner, repo: repository, workflow_id: WORKFLOW_ID, event: 'workflow_dispatch', per_page: 10 }); + const run = response.data.workflow_runs.find(candidate => !candidate.created_at || new Date(candidate.created_at).getTime() >= startedAt - 5_000); + if (run) { + while (run.status && run.status !== 'completed' && Date.now() <= deadline) { + await this.sleep(this.pollMs); + const latest = await client.rest.actions.getWorkflowRun({ owner, repo: repository, run_id: run.id }); + Object.assign(run, latest.data); + } + return run; + } + await this.sleep(this.pollMs); + } + return undefined; + } +} + +function healthStatus( + requirement: SetupCredentialRequirement, + jobs: ReadonlyMap, +): SetupCredentialCheck['status'] { + if (!INPUT_BY_SECRET[requirement.name]) return 'unverifiable'; + const job = jobs.get(JOB_BY_SECRET[requirement.name]); + if (!job) return 'unverifiable'; + return job.conclusion === 'success' ? 'valid' : job.conclusion ? 'invalid' : 'unverifiable'; +} + +function healthMessage( + requirement: SetupCredentialRequirement, + jobs: ReadonlyMap, +): string { + if (!INPUT_BY_SECRET[requirement.name]) return 'No remote health check is implemented for this provider.'; + const job = jobs.get(JOB_BY_SECRET[requirement.name]); + if (!job) return 'Remote credential health workflow did not report this credential separately.'; + return job.conclusion === 'success' + ? 'Remote credential health check passed.' + : job.conclusion + ? `Remote credential health check failed (${job.conclusion}).` + : 'Remote credential health check is still incomplete.'; +} + +function isNotFound(error: unknown): boolean { + return Boolean(error && typeof error === 'object' && 'status' in error && (error as { status?: unknown }).status === 404); +} + +function readHealthWorkflow(): string { + try { + return readFileSync(path.join(__dirname, '..', '..', 'setup', 'workflows', WORKFLOW_ID), 'utf8'); + } catch { + return ''; + } +} diff --git a/src/infrastructure/setup_workspace_adapter.ts b/src/infrastructure/setup_workspace_adapter.ts index d4b87cb3..9d91c7ff 100644 --- a/src/infrastructure/setup_workspace_adapter.ts +++ b/src/infrastructure/setup_workspace_adapter.ts @@ -1,14 +1,24 @@ -import { copySetupFiles, ensureGitHubDirs, hasValidSetupToken } from '../utils/setup_files'; -import type { SetupWorkspacePort, SetupWorkspaceResult } from '../application/ports/setup_workspace_ports'; +import { copySetupFiles, ensureGitHubDirs, hasValidSetupToken, compareSetupWorkflows } from '../utils/setup_files'; +import type { SetupWorkspacePort, SetupWorkspaceResult, SetupWorkspaceSelection } from '../application/ports/setup_workspace_ports'; export class SetupWorkspaceAdapter implements SetupWorkspacePort { - prepare(): SetupWorkspaceResult { + prepare(selection?: SetupWorkspaceSelection): SetupWorkspaceResult { const workspace = process.cwd(); ensureGitHubDirs(workspace); - return copySetupFiles(workspace); + if (!selection) return copySetupFiles(workspace); + return copySetupFiles(workspace, undefined, selection?.features, { + updateExistingWorkflows: selection?.updateExistingWorkflows, + approvedWorkflowFiles: selection?.approvedWorkflowFiles, + }); } - hasValidToken(): boolean { - return hasValidSetupToken(process.cwd()); + hasValidToken(tokenOverride?: string): boolean { + return tokenOverride === undefined + ? hasValidSetupToken(process.cwd()) + : hasValidSetupToken(process.cwd(), tokenOverride); + } + + compareWorkflows(features?: Parameters[1]): ReturnType { + return compareSetupWorkflows(process.cwd(), features); } } diff --git a/src/utils/__tests__/setup_files.test.ts b/src/utils/__tests__/setup_files.test.ts index e5980d41..d9e19edc 100644 --- a/src/utils/__tests__/setup_files.test.ts +++ b/src/utils/__tests__/setup_files.test.ts @@ -1,300 +1,83 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { ensureGitHubDirs, copySetupFiles, ensureEnvWithToken, getSetupToken, hasValidSetupToken, setupEnvFileExists } from '../setup_files'; +import { ensureGitHubDirs, copySetupFiles, getSetupToken, hasValidSetupToken, compareSetupWorkflows } from '../setup_files'; -jest.mock('../logger', () => ({ - logInfo: jest.fn(), -})); - -const ENV_TOKEN_KEY = 'PERSONAL_ACCESS_TOKEN'; -const ENV_PLACEHOLDER = 'PERSONAL_ACCESS_TOKEN=github_pat_11..'; +jest.mock('../logger', () => ({ logInfo: jest.fn() })); describe('setup_files', () => { let tmpDir: string; - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup_files_test_')); - }); + beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup_files_test_')); }); + afterEach(() => { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); }); - afterEach(() => { - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } + it('creates the GitHub directories used by setup', () => { + ensureGitHubDirs(tmpDir); + expect(fs.existsSync(path.join(tmpDir, '.github', 'workflows'))).toBe(true); + expect(fs.existsSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE'))).toBe(true); }); - describe('ensureGitHubDirs', () => { - it('creates .github, .github/workflows and .github/ISSUE_TEMPLATE when they do not exist', () => { - ensureGitHubDirs(tmpDir); - expect(fs.existsSync(path.join(tmpDir, '.github'))).toBe(true); - expect(fs.existsSync(path.join(tmpDir, '.github', 'workflows'))).toBe(true); - expect(fs.existsSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE'))).toBe(true); - }); - - it('does not fail when directories already exist', () => { - fs.mkdirSync(path.join(tmpDir, '.github'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE'), { recursive: true }); - expect(() => ensureGitHubDirs(tmpDir)).not.toThrow(); - expect(fs.existsSync(path.join(tmpDir, '.github', 'workflows'))).toBe(true); - }); + it('copies setup files and never creates a local credential file', () => { + const setupDir = path.join(tmpDir, 'setup'); + fs.mkdirSync(path.join(setupDir, 'workflows'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true }); + fs.writeFileSync(path.join(setupDir, 'workflows', 'ci.yml'), 'name: test'); + expect(copySetupFiles(tmpDir, setupDir)).toEqual({ copied: 1, skipped: 0 }); + expect(fs.existsSync(path.join(tmpDir, '.env'))).toBe(false); }); - describe('copySetupFiles', () => { - const setupDir = () => path.join(tmpDir, 'setup'); - - it('returns { copied: 0, skipped: 0 } when setup/ does not exist', () => { - const result = copySetupFiles(tmpDir, setupDir()); - expect(result).toEqual({ copied: 0, skipped: 0 }); - }); - - it('copies workflow yml files from setup/workflows to .github/workflows', () => { - fs.mkdirSync(path.join(tmpDir, 'setup', 'workflows'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true }); - const workflowContent = 'name: test'; - fs.writeFileSync(path.join(tmpDir, 'setup', 'workflows', 'ci.yml'), workflowContent); - const result = copySetupFiles(tmpDir, setupDir()); - expect(result.copied).toBe(1); - expect(fs.readFileSync(path.join(tmpDir, '.github', 'workflows', 'ci.yml'), 'utf8')).toBe(workflowContent); - }); - - it('skips workflow file when destination already exists', () => { - fs.mkdirSync(path.join(tmpDir, 'setup', 'workflows'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true }); - fs.writeFileSync(path.join(tmpDir, 'setup', 'workflows', 'ci.yml'), 'from-setup'); - fs.writeFileSync(path.join(tmpDir, '.github', 'workflows', 'ci.yml'), 'existing'); - const result = copySetupFiles(tmpDir, setupDir()); - expect(result.skipped).toBe(1); - expect(result.copied).toBe(0); - expect(fs.readFileSync(path.join(tmpDir, '.github', 'workflows', 'ci.yml'), 'utf8')).toBe('existing'); - }); - - it('copies ISSUE_TEMPLATE files when setup/ISSUE_TEMPLATE exists', () => { - fs.mkdirSync(path.join(tmpDir, 'setup', 'ISSUE_TEMPLATE'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE'), { recursive: true }); - fs.writeFileSync(path.join(tmpDir, 'setup', 'ISSUE_TEMPLATE', 'bug_report.yml'), 'title: Bug'); - const result = copySetupFiles(tmpDir, setupDir()); - expect(result.copied).toBe(1); - expect(fs.readFileSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE', 'bug_report.yml'), 'utf8')).toBe('title: Bug'); - }); - - it('copies pull_request_template.md when it exists in setup/', () => { - fs.mkdirSync(path.join(tmpDir, 'setup'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github'), { recursive: true }); - fs.writeFileSync(path.join(tmpDir, 'setup', 'pull_request_template.md'), '# PR template'); - const result = copySetupFiles(tmpDir, setupDir()); - expect(result.copied).toBe(1); - expect(fs.readFileSync(path.join(tmpDir, '.github', 'pull_request_template.md'), 'utf8')).toBe('# PR template'); - }); - - it('skips pull_request_template.md when destination already exists', () => { - fs.mkdirSync(path.join(tmpDir, 'setup'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github'), { recursive: true }); - fs.writeFileSync(path.join(tmpDir, 'setup', 'pull_request_template.md'), '# from setup'); - fs.writeFileSync(path.join(tmpDir, '.github', 'pull_request_template.md'), '# existing'); - const result = copySetupFiles(tmpDir, setupDir()); - expect(result.skipped).toBe(1); - expect(fs.readFileSync(path.join(tmpDir, '.github', 'pull_request_template.md'), 'utf8')).toBe('# existing'); - }); - - it('does not create .env when no token in env and no .env (only suggests via log)', () => { - const saved = process.env[ENV_TOKEN_KEY]; - delete process.env[ENV_TOKEN_KEY]; - try { - fs.mkdirSync(path.join(tmpDir, 'setup'), { recursive: true }); - copySetupFiles(tmpDir, setupDir()); - expect(fs.existsSync(path.join(tmpDir, '.env'))).toBe(false); - } finally { - if (saved !== undefined) process.env[ENV_TOKEN_KEY] = saved; - } - }); - - it('does not overwrite .env when it already exists', () => { - const saved = process.env[ENV_TOKEN_KEY]; - delete process.env[ENV_TOKEN_KEY]; - try { - fs.mkdirSync(path.join(tmpDir, 'setup'), { recursive: true }); - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=existing_token'); - copySetupFiles(tmpDir, setupDir()); - expect(fs.readFileSync(path.join(tmpDir, '.env'), 'utf8')).toBe('PERSONAL_ACCESS_TOKEN=existing_token'); - } finally { - if (saved !== undefined) process.env[ENV_TOKEN_KEY] = saved; - } - }); - - it('skips existing ISSUE_TEMPLATE file and copies non-existing one', () => { - fs.mkdirSync(path.join(tmpDir, 'setup', 'ISSUE_TEMPLATE'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE'), { recursive: true }); - fs.writeFileSync(path.join(tmpDir, 'setup', 'ISSUE_TEMPLATE', 'existing.yml'), 'existing'); - fs.writeFileSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE', 'existing.yml'), 'already-there'); - fs.writeFileSync(path.join(tmpDir, 'setup', 'ISSUE_TEMPLATE', 'new.yml'), 'new'); - const result = copySetupFiles(tmpDir, setupDir()); - expect(result.copied).toBe(1); - expect(result.skipped).toBe(1); - expect(fs.readFileSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE', 'existing.yml'), 'utf8')).toBe('already-there'); - expect(fs.readFileSync(path.join(tmpDir, '.github', 'ISSUE_TEMPLATE', 'new.yml'), 'utf8')).toBe('new'); - }); - - it('skips workflow file that is a directory', () => { - fs.mkdirSync(path.join(tmpDir, 'setup', 'workflows'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true }); - fs.mkdirSync(path.join(tmpDir, 'setup', 'workflows', 'ci.yml'), { recursive: true }); - const result = copySetupFiles(tmpDir, setupDir()); - expect(result.copied).toBe(0); - expect(fs.statSync(path.join(tmpDir, 'setup', 'workflows', 'ci.yml')).isDirectory()).toBe(true); - }); + it('keeps existing workflows by default and reports their state', () => { + const setupDir = path.join(tmpDir, 'setup'); + fs.mkdirSync(path.join(setupDir, 'workflows'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true }); + fs.writeFileSync(path.join(setupDir, 'workflows', 'ci.yml'), 'new'); + fs.writeFileSync(path.join(tmpDir, '.github', 'workflows', 'ci.yml'), 'old'); + expect(copySetupFiles(tmpDir, setupDir)).toEqual({ copied: 0, skipped: 1 }); + expect(compareSetupWorkflows(tmpDir, undefined, setupDir)).toEqual([ + { file: 'ci.yml', destination: '.github/workflows/ci.yml', status: 'changed' }, + ]); }); - describe('ensureEnvWithToken', () => { - let savedToken: string | undefined; - - beforeEach(() => { - savedToken = process.env[ENV_TOKEN_KEY]; - }); - - afterEach(() => { - if (savedToken !== undefined) { - process.env[ENV_TOKEN_KEY] = savedToken; - } else { - delete process.env[ENV_TOKEN_KEY]; - } - }); - - it('does not create .env when PERSONAL_ACCESS_TOKEN is set in environment', () => { - process.env[ENV_TOKEN_KEY] = 'env_token'; - ensureEnvWithToken(tmpDir); - expect(fs.existsSync(path.join(tmpDir, '.env'))).toBe(false); - }); - - it('does not create .env when no token in env and no .env exists (only suggests via log)', () => { - delete process.env[ENV_TOKEN_KEY]; - ensureEnvWithToken(tmpDir); - expect(fs.existsSync(path.join(tmpDir, '.env'))).toBe(false); - }); - - it('does not overwrite .env when it exists with PERSONAL_ACCESS_TOKEN set', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=my_gh_token'); - ensureEnvWithToken(tmpDir); - expect(fs.readFileSync(path.join(tmpDir, '.env'), 'utf8')).toBe('PERSONAL_ACCESS_TOKEN=my_gh_token'); - }); - - it('does not overwrite .env when it exists but PERSONAL_ACCESS_TOKEN is empty', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=\n'); - ensureEnvWithToken(tmpDir); - expect(fs.readFileSync(path.join(tmpDir, '.env'), 'utf8')).toBe('PERSONAL_ACCESS_TOKEN=\n'); - }); + it('updates only approved workflows and preserves a backup', () => { + const setupDir = path.join(tmpDir, 'setup'); + fs.mkdirSync(path.join(setupDir, 'workflows'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true }); + fs.writeFileSync(path.join(setupDir, 'workflows', 'ci.yml'), 'new'); + fs.writeFileSync(path.join(tmpDir, '.github', 'workflows', 'ci.yml'), 'old'); + const result = copySetupFiles(tmpDir, setupDir, undefined, { + updateExistingWorkflows: true, + approvedWorkflowFiles: ['ci.yml'], + }); + expect(result).toEqual({ copied: 1, skipped: 0 }); + expect(fs.readFileSync(path.join(tmpDir, '.github', 'workflows', 'ci.yml'), 'utf8')).toBe('new'); + const backups = fs.readdirSync(path.join(tmpDir, '.copilot', 'setup-backups')); + expect(backups).toHaveLength(1); + expect(fs.readFileSync(path.join(tmpDir, '.copilot', 'setup-backups', backups[0], 'ci.yml'), 'utf8')).toBe('old'); }); - describe('hasValidSetupToken', () => { - let savedToken: string | undefined; + describe('setup token resolution', () => { + const key = 'PERSONAL_ACCESS_TOKEN'; + let previous: string | undefined; + beforeEach(() => { previous = process.env[key]; }); + afterEach(() => { if (previous === undefined) delete process.env[key]; else process.env[key] = previous; }); - beforeEach(() => { - savedToken = process.env[ENV_TOKEN_KEY]; + it('uses an explicit token before the environment', () => { + process.env[key] = 'ghp_environment_token_xxxxxxxxxxxx'; + expect(getSetupToken(tmpDir, 'ghp_explicit_token_xxxxxxxxxxxx')).toBe('ghp_explicit_token_xxxxxxxxxxxx'); }); - afterEach(() => { - if (savedToken !== undefined) { - process.env[ENV_TOKEN_KEY] = savedToken; - } else { - delete process.env[ENV_TOKEN_KEY]; - } - }); - - it('returns true when PERSONAL_ACCESS_TOKEN in env has length >= 20 and is not placeholder', () => { - process.env[ENV_TOKEN_KEY] = 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; - expect(hasValidSetupToken(tmpDir)).toBe(true); - }); - - it('returns false when PERSONAL_ACCESS_TOKEN in env is the placeholder', () => { - process.env[ENV_TOKEN_KEY] = 'github_pat_11..'; + it('uses only the environment and never reads .env', () => { + delete process.env[key]; + fs.writeFileSync(path.join(tmpDir, '.env'), `${key}=ghp_file_token_xxxxxxxxxxxxxxxxxxxx`); + expect(getSetupToken(tmpDir)).toBeUndefined(); expect(hasValidSetupToken(tmpDir)).toBe(false); }); - it('returns false when PERSONAL_ACCESS_TOKEN in env is too short', () => { - process.env[ENV_TOKEN_KEY] = 'short'; + it('rejects placeholders and accepts a sufficiently long token', () => { + process.env[key] = 'github_pat_11..'; expect(hasValidSetupToken(tmpDir)).toBe(false); - }); - - it('returns true when .env has valid token and env is not set', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); + process.env[key] = 'ghp_valid_token_xxxxxxxxxxxxxxxxxxxx'; expect(hasValidSetupToken(tmpDir)).toBe(true); }); - - it('returns false when .env has only placeholder and env is not set', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=github_pat_11..'); - expect(hasValidSetupToken(tmpDir)).toBe(false); - }); - - it('falls back to .env when env is set but invalid (placeholder); then returns true', () => { - process.env[ENV_TOKEN_KEY] = 'github_pat_11..'; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); - expect(hasValidSetupToken(tmpDir)).toBe(true); - }); - - it('returns true when valid override is passed (e.g. CLI --token)', () => { - delete process.env[ENV_TOKEN_KEY]; - expect(hasValidSetupToken(tmpDir, 'ghp_override_token_xxxxxxxxxxxxxxxxxx')).toBe(true); - }); - - it('falls back to env/.env when override is invalid (placeholder or too short)', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); - expect(hasValidSetupToken(tmpDir, 'github_pat_11..')).toBe(true); - expect(hasValidSetupToken(tmpDir, 'short')).toBe(true); - }); - }); - - describe('getSetupToken', () => { - it('returns token from env when valid', () => { - process.env[ENV_TOKEN_KEY] = 'ghp_abcdefghijklmnopqrstuvwxyz123456'; - expect(getSetupToken(tmpDir)).toBe('ghp_abcdefghijklmnopqrstuvwxyz123456'); - delete process.env[ENV_TOKEN_KEY]; - }); - - it('returns token from .env when env is not set', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); - expect(getSetupToken(tmpDir)).toBe('ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); - }); - - it('returns undefined when no valid token', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=github_pat_11..'); - expect(getSetupToken(tmpDir)).toBeUndefined(); - }); - - it('returns valid override first (CLI token priority)', () => { - delete process.env[ENV_TOKEN_KEY]; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=ghp_from_env_file_xxxxxxxxxxxxxxxxxx'); - expect(getSetupToken(tmpDir, 'ghp_cli_token_xxxxxxxxxxxxxxxxxxxxxxxx')).toBe('ghp_cli_token_xxxxxxxxxxxxxxxxxxxxxxxx'); - }); - - it('falls back to env then .env when override is invalid', () => { - process.env[ENV_TOKEN_KEY] = 'ghp_from_env_xxxxxxxxxxxxxxxxxxxxxxxxxx'; - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=ghp_from_file_xxxxxxxxxxxxxxxxxxxx'); - expect(getSetupToken(tmpDir, 'github_pat_11..')).toBe('ghp_from_env_xxxxxxxxxxxxxxxxxxxxxxxxxx'); - delete process.env[ENV_TOKEN_KEY]; - expect(getSetupToken(tmpDir, 'short')).toBe('ghp_from_file_xxxxxxxxxxxxxxxxxxxx'); - }); - }); - - describe('setupEnvFileExists', () => { - it('returns true when .env file exists', () => { - fs.writeFileSync(path.join(tmpDir, '.env'), 'PERSONAL_ACCESS_TOKEN=token'); - expect(setupEnvFileExists(tmpDir)).toBe(true); - }); - - it('returns false when .env does not exist', () => { - expect(setupEnvFileExists(tmpDir)).toBe(false); - }); - - it('returns false when .env is a directory', () => { - fs.mkdirSync(path.join(tmpDir, '.env'), { recursive: true }); - expect(setupEnvFileExists(tmpDir)).toBe(false); - }); }); }); diff --git a/src/utils/setup_file_copy.ts b/src/utils/setup_file_copy.ts index 7163cd93..83354174 100644 --- a/src/utils/setup_file_copy.ts +++ b/src/utils/setup_file_copy.ts @@ -4,14 +4,23 @@ import { logInfo } from './logger'; export type CopyStats = { copied: number; skipped: number }; -export function copySetupFile(source: string, destination: string, displaySource: string, displayDestination: string): CopyStats { +export interface CopyOptions { + overwrite?: boolean; + backupDirectory?: string; +} + +export function copySetupFile(source: string, destination: string, displaySource: string, displayDestination: string, options: CopyOptions = {}): CopyStats { if (!fs.existsSync(source)) return { copied: 0, skipped: 0 }; - if (fs.existsSync(destination)) { + if (fs.existsSync(destination) && !options.overwrite) { logInfo(` ⏭️ ${displayDestination} already exists; skipping.`); return { copied: 0, skipped: 1 }; } + if (fs.existsSync(destination) && options.backupDirectory) { + fs.mkdirSync(options.backupDirectory, { recursive: true }); + fs.copyFileSync(destination, path.join(options.backupDirectory, path.basename(destination))); + } fs.copyFileSync(source, destination); - logInfo(` ✅ Copied ${displaySource} → ${displayDestination}`); + logInfo(` ${options.overwrite ? '↻ Updated' : '✅ Copied'} ${displaySource} → ${displayDestination}`); return { copied: 1, skipped: 0 }; } @@ -20,6 +29,7 @@ export function copySetupDirectory( destinationDirectory: string, fileFilter: (fileName: string) => boolean, displayDirectory: string, + options: CopyOptions = {}, ): CopyStats { if (!fs.existsSync(sourceDirectory)) return { copied: 0, skipped: 0 }; return fs.readdirSync(sourceDirectory) @@ -30,6 +40,7 @@ export function copySetupDirectory( path.join(destinationDirectory, fileName), `${displayDirectory}/${fileName}`, `${displayDirectory.replace('setup/', '.github/')}/${fileName}`, + options, )) .reduce((total, current) => ({ copied: total.copied + current.copied, diff --git a/src/utils/setup_files.ts b/src/utils/setup_files.ts index abe439de..281dd099 100644 --- a/src/utils/setup_files.ts +++ b/src/utils/setup_files.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { copySetupDirectory, copySetupFile } from './setup_file_copy'; import { logInfo } from './logger'; +import type { SetupFeatures, SetupWorkflowComparison } from '../domain/setup'; /** * Ensure .github, .github/workflows and .github/ISSUE_TEMPLATE exist; create them if missing. @@ -34,73 +35,100 @@ export function ensureGitHubDirs(cwd: string): void { * @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root. * @returns { copied, skipped } */ -export function copySetupFiles(cwd: string, setupDirOverride?: string): { copied: number; skipped: number } { +export function copySetupFiles( + cwd: string, + setupDirOverride?: string, + features?: SetupFeatures, + options: { updateExistingWorkflows?: boolean; approvedWorkflowFiles?: readonly string[] } = {}, +): { copied: number; skipped: number } { const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup'); if (!fs.existsSync(setupDir)) return { copied: 0, skipped: 0 }; + const workflowFeatures: Readonly> = { + 'copilot_issue.yml': 'issues', + 'copilot_pull_request.yml': 'pullRequests', + 'copilot_commit.yml': 'commits', + 'copilot_issue_comment.yml': 'issueComments', + 'copilot_pull_request_comment.yml': 'pullRequestComments', + 'release_workflow.yml': 'release', + 'hotfix_workflow.yml': 'hotfix', + 'agent-cli-provisioning.yml': 'agentProvisioning', + 'copilot_credential_health.yml': 'credentialHealth', + }; + const approvedWorkflowFiles = new Set(options.approvedWorkflowFiles ?? []); + const backupDirectory = options.updateExistingWorkflows ? path.join(cwd, '.copilot', 'setup-backups', new Date().toISOString().replace(/[:.]/g, '-')) : undefined; const workflows = copySetupDirectory( path.join(setupDir, 'workflows'), path.join(cwd, '.github', 'workflows'), - (fileName) => fileName.endsWith('.yml') || fileName.endsWith('.yaml'), + (fileName) => (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) + && (features === undefined || features[workflowFeatures[fileName]] !== false) + && (!options.updateExistingWorkflows + || approvedWorkflowFiles.has(fileName) + || !fs.existsSync(path.join(cwd, '.github', 'workflows', fileName))), 'setup/workflows', + { + overwrite: options.updateExistingWorkflows, + backupDirectory, + }, ); const issueTemplates = copySetupDirectory( path.join(setupDir, 'ISSUE_TEMPLATE'), path.join(cwd, '.github', 'ISSUE_TEMPLATE'), - () => true, + (fileName) => features?.issueTemplates !== false + && (features?.release !== false || fileName !== 'release.yml') + && (features?.hotfix !== false || fileName !== 'hotfix.yml'), 'setup/ISSUE_TEMPLATE', ); - const pullRequestTemplate = copySetupFile( - path.join(setupDir, 'pull_request_template.md'), - path.join(cwd, '.github', 'pull_request_template.md'), - 'setup/pull_request_template.md', - '.github/pull_request_template.md', - ); - // Credentials are deliberately never copied from the package. Keep the - // destination check here so setup can guide users to their local .env. - ensureEnvWithToken(cwd); + const pullRequestTemplate = features?.pullRequestTemplate === false + ? { copied: 0, skipped: 0 } + : copySetupFile( + path.join(setupDir, 'pull_request_template.md'), + path.join(cwd, '.github', 'pull_request_template.md'), + 'setup/pull_request_template.md', + '.github/pull_request_template.md', + ); return [workflows, issueTemplates, pullRequestTemplate].reduce((total, current) => ({ copied: total.copied + current.copied, skipped: total.skipped + current.skipped, }), { copied: 0, skipped: 0 }); } +export function compareSetupWorkflows( + cwd: string, + features?: SetupFeatures, + setupDirOverride?: string, +): SetupWorkflowComparison[] { + const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup'); + const workflowFeatures: Readonly> = { + 'copilot_issue.yml': 'issues', + 'copilot_pull_request.yml': 'pullRequests', + 'copilot_commit.yml': 'commits', + 'copilot_issue_comment.yml': 'issueComments', + 'copilot_pull_request_comment.yml': 'pullRequestComments', + 'release_workflow.yml': 'release', + 'hotfix_workflow.yml': 'hotfix', + 'agent-cli-provisioning.yml': 'agentProvisioning', + 'copilot_credential_health.yml': 'credentialHealth', + }; + const sourceDirectory = path.join(setupDir, 'workflows'); + if (!fs.existsSync(sourceDirectory)) return []; + return fs.readdirSync(sourceDirectory) + .filter(file => (file.endsWith('.yml') || file.endsWith('.yaml')) && (features === undefined || features[workflowFeatures[file]] !== false)) + .filter(file => fs.statSync(path.join(sourceDirectory, file)).isFile()) + .map(file => { + const source = path.join(sourceDirectory, file); + const destination = path.join(cwd, '.github', 'workflows', file); + if (!fs.existsSync(destination)) return { file, destination: `.github/workflows/${file}`, status: 'missing' as const }; + const equal = fs.readFileSync(source, 'utf8') === fs.readFileSync(destination, 'utf8'); + return { file, destination: `.github/workflows/${file}`, status: equal ? 'unchanged' as const : 'changed' as const }; + }); +} + const ENV_TOKEN_KEY = 'PERSONAL_ACCESS_TOKEN'; const ENV_PLACEHOLDER_VALUE = 'github_pat_11..'; /** Minimum length for a token to be considered "defined" (not placeholder). */ const MIN_VALID_TOKEN_LENGTH = 20; -function getTokenFromEnvFile(envPath: string): string | null { - if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) return null; - const content = fs.readFileSync(envPath, 'utf8'); - const match = content.match(new RegExp(`^${ENV_TOKEN_KEY}=(.+)$`, 'm')); - if (!match) return null; - const value = match[1].trim().replace(/^["']|["']$/g, ''); - return value.length > 0 ? value : null; -} - -/** - * Logs the current state of PERSONAL_ACCESS_TOKEN (environment or .env). Does not create .env. - */ -export function ensureEnvWithToken(cwd: string): void { - const envPath = path.join(cwd, '.env'); - const tokenInEnv = process.env[ENV_TOKEN_KEY]?.trim(); - if (tokenInEnv) { - logInfo(' 🔑 PERSONAL_ACCESS_TOKEN is set in environment; .env not needed.'); - return; - } - if (fs.existsSync(envPath)) { - const tokenInFile = getTokenFromEnvFile(envPath); - if (tokenInFile) { - logInfo(' ✅ .env exists and contains PERSONAL_ACCESS_TOKEN.'); - } else { - logInfo(' ⚠️ .env exists but PERSONAL_ACCESS_TOKEN is missing or empty.'); - } - return; - } - logInfo(' 💡 You can create a .env file here with PERSONAL_ACCESS_TOKEN=your_token or set it in your environment.'); -} - function isTokenValueValid(token: string): boolean { const t = token.trim(); return t.length >= MIN_VALID_TOKEN_LENGTH && t !== ENV_PLACEHOLDER_VALUE; @@ -109,18 +137,14 @@ function isTokenValueValid(token: string): boolean { /** * Resolves the PERSONAL_ACCESS_TOKEN for setup from a single priority order: * 1. override (e.g. CLI --token) if provided and valid, - * 2. process.env.PERSONAL_ACCESS_TOKEN, - * 3. .env file in cwd. + * 2. process.env.PERSONAL_ACCESS_TOKEN. * Returns undefined if no valid token is found. */ -export function getSetupToken(cwd: string, override?: string): string | undefined { +export function getSetupToken(_cwd: string, override?: string): string | undefined { const overrideTrimmed = override?.trim(); if (overrideTrimmed && isTokenValueValid(overrideTrimmed)) return overrideTrimmed; const fromEnv = process.env[ENV_TOKEN_KEY]?.trim(); if (fromEnv && isTokenValueValid(fromEnv)) return fromEnv; - const envPath = path.join(cwd, '.env'); - const fromFile = getTokenFromEnvFile(envPath); - if (fromFile !== null && isTokenValueValid(fromFile)) return fromFile; return undefined; } @@ -131,9 +155,3 @@ export function getSetupToken(cwd: string, override?: string): string | undefine export function hasValidSetupToken(cwd: string, override?: string): boolean { return getSetupToken(cwd, override) !== undefined; } - -/** Returns true if a .env file exists in the given directory. */ -export function setupEnvFileExists(cwd: string): boolean { - const envPath = path.join(cwd, '.env'); - return fs.existsSync(envPath) && fs.statSync(envPath).isFile(); -} From d04777f0c466bf90f8aa244294ad89c8dc8cd5d8 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Thu, 3 Sep 2026 03:23:27 +0200 Subject: [PATCH 03/11] master: add CLI update checks and documentation validation --- .github/workflows/ci_check.yml | 3 + README.md | 2 +- build/cli/index.js | 282 +++++++++++++++++- .../ports/cli_update_check_ports.d.ts | 4 + .../usecases/check_cli_update_use_case.d.ts | 11 + build/cli/src/cli/cli_program.d.ts | 3 +- .../cli/src/cli/cli_update_check_policy.d.ts | 3 + .../cli/src/cli/cli_update_notification.d.ts | 6 + build/cli/src/domain/cli_version.d.ts | 4 + .../cli/npm_cli_update_check_adapter.d.ts | 37 +++ .../cli_update_check_composition_root.d.ts | 2 + .../ports/cli_update_check_ports.d.ts | 4 + .../usecases/check_cli_update_use_case.d.ts | 11 + build/github_action/src/cli/cli_program.d.ts | 3 +- .../src/cli/cli_update_check_policy.d.ts | 3 + .../src/cli/cli_update_notification.d.ts | 6 + .../github_action/src/domain/cli_version.d.ts | 4 + .../cli/npm_cli_update_check_adapter.d.ts | 37 +++ .../cli_update_check_composition_root.d.ts | 2 + docs.json | 20 ++ docs/agents/cli-configuration.mdx | 2 +- docs/agents/codex-openai.mdx | 2 +- docs/agents/opencode.mdx | 6 +- docs/authentication.mdx | 2 +- docs/bugbot/examples.mdx | 10 +- docs/configuration.mdx | 76 +++++ .../documentation-completeness-plan.mdx | 6 +- docs/development/documentation.mdx | 5 +- docs/development/release-process.mdx | 1 + docs/development/testing.mdx | 1 + docs/features.mdx | 4 +- docs/how-to-use.mdx | 37 ++- docs/issues/assignees-and-projects.mdx | 4 +- docs/issues/branch-management.mdx | 6 +- docs/issues/examples.mdx | 6 +- docs/issues/type/bugfix.mdx | 16 +- docs/issues/type/chore.mdx | 16 +- docs/issues/type/docs.mdx | 18 +- docs/issues/type/feature.mdx | 16 +- docs/issues/type/hotfix.mdx | 24 +- docs/issues/type/release.mdx | 24 +- docs/issues/workflow-setup.mdx | 2 +- docs/pull-requests/ai-description.mdx | 4 +- docs/pull-requests/configuration.mdx | 2 +- docs/pull-requests/examples.mdx | 10 +- docs/pull-requests/workflow-setup.mdx | 2 +- docs/quick-start.mdx | 4 +- .../operations/cli-provisioning.mdx | 2 +- .../operations/provisioning.mdx | 6 +- .../operations/troubleshooting.mdx | 2 +- .../operations/upgrade-rollback.mdx | 2 +- .../operations/version-pinning.mdx | 6 +- .../security/credentials.mdx | 6 +- docs/single-actions/available-actions.mdx | 2 +- docs/single-actions/configuration.mdx | 4 +- docs/single-actions/examples.mdx | 18 +- docs/single-actions/workflow-and-cli.mdx | 6 +- package.json | 1 + scripts/validate-documentation-contract.cjs | 88 ++++++ .../ports/cli_update_check_ports.ts | 4 + .../check_cli_update_use_case.test.ts | 24 ++ .../usecases/check_cli_update_use_case.ts | 19 ++ .../cli_program_update_check.test.ts | 24 ++ .../__tests__/cli_update_check_policy.test.ts | 20 ++ .../__tests__/cli_update_notification.test.ts | 24 ++ src/cli/cli_program.ts | 14 +- src/cli/cli_update_check_policy.ts | 13 + src/cli/cli_update_notification.ts | 21 ++ src/domain/__tests__/cli_version.test.ts | 30 ++ src/domain/cli_version.ts | 63 ++++ .../npm_cli_update_check_adapter.test.ts | 115 +++++++ .../cli/npm_cli_update_check_adapter.ts | 133 +++++++++ .../cli_update_check_composition_root.ts | 6 + 73 files changed, 1272 insertions(+), 134 deletions(-) create mode 100644 build/cli/src/application/ports/cli_update_check_ports.d.ts create mode 100644 build/cli/src/application/usecases/check_cli_update_use_case.d.ts create mode 100644 build/cli/src/cli/cli_update_check_policy.d.ts create mode 100644 build/cli/src/cli/cli_update_notification.d.ts create mode 100644 build/cli/src/domain/cli_version.d.ts create mode 100644 build/cli/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts create mode 100644 build/cli/src/infrastructure/composition/cli_update_check_composition_root.d.ts create mode 100644 build/github_action/src/application/ports/cli_update_check_ports.d.ts create mode 100644 build/github_action/src/application/usecases/check_cli_update_use_case.d.ts create mode 100644 build/github_action/src/cli/cli_update_check_policy.d.ts create mode 100644 build/github_action/src/cli/cli_update_notification.d.ts create mode 100644 build/github_action/src/domain/cli_version.d.ts create mode 100644 build/github_action/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts create mode 100644 build/github_action/src/infrastructure/composition/cli_update_check_composition_root.d.ts create mode 100644 scripts/validate-documentation-contract.cjs create mode 100644 src/application/ports/cli_update_check_ports.ts create mode 100644 src/application/usecases/__tests__/check_cli_update_use_case.test.ts create mode 100644 src/application/usecases/check_cli_update_use_case.ts create mode 100644 src/cli/__tests__/cli_program_update_check.test.ts create mode 100644 src/cli/__tests__/cli_update_check_policy.test.ts create mode 100644 src/cli/__tests__/cli_update_notification.test.ts create mode 100644 src/cli/cli_update_check_policy.ts create mode 100644 src/cli/cli_update_notification.ts create mode 100644 src/domain/__tests__/cli_version.test.ts create mode 100644 src/domain/cli_version.ts create mode 100644 src/infrastructure/cli/__tests__/npm_cli_update_check_adapter.test.ts create mode 100644 src/infrastructure/cli/npm_cli_update_check_adapter.ts create mode 100644 src/infrastructure/composition/cli_update_check_composition_root.ts diff --git a/.github/workflows/ci_check.yml b/.github/workflows/ci_check.yml index 7d4f6e6d..ff264004 100644 --- a/.github/workflows/ci_check.yml +++ b/.github/workflows/ci_check.yml @@ -81,6 +81,9 @@ jobs: - name: Validate documentation assets run: pnpm run validate:docs-page + - name: Validate documentation contract + run: pnpm run validate:documentation + - name: Validate workflow contract run: pnpm run validate:workflows diff --git a/README.md b/README.md index 770e4368..a70d61a3 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Full documentation: **[docs.page/vypdev/copilot](https://docs.page/vypdev/copilo npm install --global @vypdev/copilot copilot --version ``` - Update the published CLI later with **`copilot upgrade`**. + Update the published CLI later with **`copilot upgrade`**. Normal CLI commands may show a non-blocking notice when a newer release is available. 4. **Add workflows** — Copy the files from `setup/workflows/` into your `.github/workflows/`, or run **`copilot setup`** from your repo root. The setup wizard securely prompts for its separate operator PAT and can validate/provision the workflow PAT and provider credentials. See [How to use](https://docs.page/vypdev/copilot/how-to-use). --- diff --git a/build/cli/index.js b/build/cli/index.js index c2fd53b8..11b41f25 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -58465,6 +58465,31 @@ function sameLabels(left, right) { } +/***/ }), + +/***/ 55721: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CheckCliUpdateUseCase = void 0; +const cli_version_1 = __nccwpck_require__(27089); +/** Checks for a newer published CLI version without coupling the application to npm. */ +class CheckCliUpdateUseCase { + constructor(cliUpdateCheckPort) { + this.cliUpdateCheckPort = cliUpdateCheckPort; + } + async execute(installedVersion) { + const publishedVersion = await this.cliUpdateCheckPort.getLatestPublishedVersion(); + if (!publishedVersion || !(0, cli_version_1.isNewerCliVersion)(installedVersion, publishedVersion)) + return undefined; + return { installedVersion, publishedVersion }; + } +} +exports.CheckCliUpdateUseCase = CheckCliUpdateUseCase; + + /***/ }), /***/ 42442: @@ -65637,21 +65662,76 @@ exports.createCliProgram = createCliProgram; const node_fs_1 = __nccwpck_require__(87561); const path = __importStar(__nccwpck_require__(49411)); const commander_1 = __nccwpck_require__(12239); +const cli_update_check_composition_root_1 = __nccwpck_require__(78998); const command_registry_1 = __nccwpck_require__(94415); +const cli_update_check_policy_1 = __nccwpck_require__(82434); +const cli_update_notification_1 = __nccwpck_require__(91033); function loadPackageVersion() { const packagePath = path.join(__dirname, '..', '..', 'package.json'); const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packagePath, 'utf8')); return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0'; } -function createCliProgram() { +function createCliProgram(updateChecker = (0, cli_update_check_composition_root_1.createCliUpdateCheckUseCase)()) { + const installedVersion = loadPackageVersion(); const program = new commander_1.Command() .name('copilot') .description('GitHub workflow automation and repository management CLI') - .version(loadPackageVersion(), '-V, --version', 'Display the installed Copilot version'); + .version(installedVersion, '-V, --version', 'Display the installed Copilot version'); + program.hook('preAction', async (_thisCommand, actionCommand) => { + if ((0, cli_update_check_policy_1.isUpdateCheckDisabled)() || !(0, cli_update_check_policy_1.shouldCheckForUpdates)(actionCommand.name())) + return; + await (0, cli_update_notification_1.notifyAboutCliUpdate)(updateChecker, installedVersion); + }); return (0, command_registry_1.registerCliCommands)(program); } +/***/ }), + +/***/ 82434: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.UPDATE_CHECK_DISABLED_ENV = void 0; +exports.isUpdateCheckDisabled = isUpdateCheckDisabled; +exports.shouldCheckForUpdates = shouldCheckForUpdates; +const UPDATE_CHECK_DISABLED_VALUES = new Set(['1', 'true', 'yes', 'on']); +const COMMANDS_WITHOUT_UPDATE_CHECK = new Set(['help', 'upgrade']); +exports.UPDATE_CHECK_DISABLED_ENV = 'COPILOT_DISABLE_UPDATE_CHECK'; +function isUpdateCheckDisabled(environment = process.env) { + const value = environment[exports.UPDATE_CHECK_DISABLED_ENV]?.trim().toLowerCase(); + return value !== undefined && UPDATE_CHECK_DISABLED_VALUES.has(value); +} +function shouldCheckForUpdates(commandName) { + return !COMMANDS_WITHOUT_UPDATE_CHECK.has(commandName); +} + + +/***/ }), + +/***/ 91033: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.notifyAboutCliUpdate = notifyAboutCliUpdate; +/** Displays advisory update information while keeping update failures invisible to users. */ +async function notifyAboutCliUpdate(checker, installedVersion, output = console) { + try { + const update = await checker.execute(installedVersion); + if (update) { + output.log(`A new version (${update.publishedVersion}) is available. Run "copilot upgrade".`); + } + } + catch { + // Version checks are advisory and must never change the command outcome. + } +} + + /***/ }), /***/ 95212: @@ -73663,6 +73743,74 @@ function fnv1a(value) { } +/***/ }), + +/***/ 27089: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.compareCliVersions = compareCliVersions; +exports.isNewerCliVersion = isNewerCliVersion; +const CLI_VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; +function parseCliVersion(version) { + const match = CLI_VERSION_PATTERN.exec(version.trim()); + if (!match) + return undefined; + return { + major: Number.parseInt(match[1], 10), + minor: Number.parseInt(match[2], 10), + patch: Number.parseInt(match[3], 10), + prerelease: match[4]?.split('.') ?? [], + }; +} +function comparePrereleaseIdentifiers(left, right) { + const leftNumber = /^\d+$/.test(left) ? Number.parseInt(left, 10) : undefined; + const rightNumber = /^\d+$/.test(right) ? Number.parseInt(right, 10) : undefined; + if (leftNumber !== undefined && rightNumber !== undefined) + return Math.sign(leftNumber - rightNumber); + if (leftNumber !== undefined) + return -1; + if (rightNumber !== undefined) + return 1; + return left < right ? -1 : left > right ? 1 : 0; +} +/** Compares two CLI versions using release and prerelease precedence. */ +function compareCliVersions(left, right) { + const leftVersion = parseCliVersion(left); + const rightVersion = parseCliVersion(right); + if (!leftVersion || !rightVersion) + return undefined; + for (const component of ['major', 'minor', 'patch']) { + if (leftVersion[component] !== rightVersion[component]) { + return leftVersion[component] < rightVersion[component] ? -1 : 1; + } + } + if (leftVersion.prerelease.length === 0 && rightVersion.prerelease.length > 0) + return 1; + if (leftVersion.prerelease.length > 0 && rightVersion.prerelease.length === 0) + return -1; + const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length); + for (let index = 0; index < length; index += 1) { + const leftIdentifier = leftVersion.prerelease[index]; + const rightIdentifier = rightVersion.prerelease[index]; + if (leftIdentifier === undefined) + return -1; + if (rightIdentifier === undefined) + return 1; + const comparison = comparePrereleaseIdentifiers(leftIdentifier, rightIdentifier); + if (comparison !== 0) + return comparison; + } + return 0; +} +/** Returns true only when the published version is newer than the installed one. */ +function isNewerCliVersion(installedVersion, publishedVersion) { + return compareCliVersions(installedVersion, publishedVersion) === -1; +} + + /***/ }), /***/ 77454: @@ -74005,6 +74153,120 @@ function normalizeOrigin(origin) { } +/***/ }), + +/***/ 62007: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.NpmCliUpdateCheckAdapter = exports.FileCliUpdateCheckCache = exports.UPDATE_CHECK_TIMEOUT_MS = exports.UPDATE_CHECK_CACHE_TTL_MS = exports.NPM_REGISTRY_URL = void 0; +exports.resolveUpdateCheckCachePath = resolveUpdateCheckCachePath; +const node_fs_1 = __nccwpck_require__(87561); +const node_os_1 = __nccwpck_require__(70612); +const node_path_1 = __nccwpck_require__(49411); +const npm_cli_upgrade_adapter_1 = __nccwpck_require__(97258); +exports.NPM_REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(npm_cli_upgrade_adapter_1.COPILOT_PACKAGE_NAME)}`; +exports.UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1000; +exports.UPDATE_CHECK_TIMEOUT_MS = 1500; +function resolveUpdateCheckCachePath(platform = process.platform, environment = process.env, homeDirectory = (0, node_os_1.homedir)()) { + const cacheRoot = platform === 'win32' + ? environment.LOCALAPPDATA || (0, node_path_1.join)(homeDirectory, 'AppData', 'Local') + : environment.XDG_CACHE_HOME || (0, node_path_1.join)(homeDirectory, '.cache'); + return (0, node_path_1.join)(cacheRoot, 'copilot', 'update-check.json'); +} +class FileCliUpdateCheckCache { + constructor(filePath = resolveUpdateCheckCachePath()) { + this.filePath = filePath; + } + read() { + try { + const value = JSON.parse((0, node_fs_1.readFileSync)(this.filePath, 'utf8')); + if (!value || typeof value !== 'object') + return undefined; + const entry = value; + if (typeof entry.checkedAt !== 'number' || !Number.isFinite(entry.checkedAt)) + return undefined; + return { + checkedAt: entry.checkedAt, + ...(typeof entry.latestVersion === 'string' ? { latestVersion: entry.latestVersion } : {}), + }; + } + catch { + return undefined; + } + } + write(entry) { + try { + (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(this.filePath), { recursive: true }); + (0, node_fs_1.writeFileSync)(this.filePath, `${JSON.stringify(entry)}\n`, { encoding: 'utf8', mode: 0o600 }); + } + catch { + // A cache failure must not affect the CLI command. + } + } +} +exports.FileCliUpdateCheckCache = FileCliUpdateCheckCache; +/** Reads npm's latest dist-tag with bounded latency and a non-sensitive local cache. */ +class NpmCliUpdateCheckAdapter { + constructor(options = {}) { + this.cache = options.cache ?? new FileCliUpdateCheckCache(); + this.fetcher = options.fetcher ?? fetch; + this.now = options.now ?? Date.now; + this.cacheTtlMs = options.cacheTtlMs ?? exports.UPDATE_CHECK_CACHE_TTL_MS; + this.timeoutMs = options.timeoutMs ?? exports.UPDATE_CHECK_TIMEOUT_MS; + } + async getLatestPublishedVersion() { + const checkedAt = this.now(); + let cached; + try { + cached = this.cache.read(); + } + catch { + cached = undefined; + } + if (cached && checkedAt >= cached.checkedAt && checkedAt - cached.checkedAt < this.cacheTtlMs) { + return cached.latestVersion; + } + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const response = await this.fetcher(exports.NPM_REGISTRY_URL, { + headers: { accept: 'application/json' }, + signal: controller.signal, + }); + if (!response.ok) + throw new Error(`npm registry returned HTTP ${response.status}`); + const payload = await response.json(); + const latestVersion = typeof payload['dist-tags']?.latest === 'string' + ? payload['dist-tags'].latest + : undefined; + this.writeCache({ checkedAt, ...(latestVersion ? { latestVersion } : {}) }); + return latestVersion; + } + finally { + clearTimeout(timeout); + } + } + catch { + this.writeCache({ checkedAt }); + return undefined; + } + } + writeCache(entry) { + try { + this.cache.write(entry); + } + catch { + // A cache failure must not affect the CLI command. + } + } +} +exports.NpmCliUpdateCheckAdapter = NpmCliUpdateCheckAdapter; + + /***/ }), /***/ 97258: @@ -74200,6 +74462,22 @@ function createCheckProgressCompositionRoot() { } +/***/ }), + +/***/ 78998: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createCliUpdateCheckUseCase = createCliUpdateCheckUseCase; +const check_cli_update_use_case_1 = __nccwpck_require__(55721); +const npm_cli_update_check_adapter_1 = __nccwpck_require__(62007); +function createCliUpdateCheckUseCase() { + return new check_cli_update_use_case_1.CheckCliUpdateUseCase(new npm_cli_update_check_adapter_1.NpmCliUpdateCheckAdapter()); +} + + /***/ }), /***/ 74142: diff --git a/build/cli/src/application/ports/cli_update_check_ports.d.ts b/build/cli/src/application/ports/cli_update_check_ports.d.ts new file mode 100644 index 00000000..908085f5 --- /dev/null +++ b/build/cli/src/application/ports/cli_update_check_ports.d.ts @@ -0,0 +1,4 @@ +/** Retrieves the latest published version of the Copilot CLI. */ +export interface CliUpdateCheckPort { + getLatestPublishedVersion(): Promise; +} diff --git a/build/cli/src/application/usecases/check_cli_update_use_case.d.ts b/build/cli/src/application/usecases/check_cli_update_use_case.d.ts new file mode 100644 index 00000000..5bbe9290 --- /dev/null +++ b/build/cli/src/application/usecases/check_cli_update_use_case.d.ts @@ -0,0 +1,11 @@ +import type { CliUpdateCheckPort } from '../ports/cli_update_check_ports'; +export interface CliUpdateAvailable { + installedVersion: string; + publishedVersion: string; +} +/** Checks for a newer published CLI version without coupling the application to npm. */ +export declare class CheckCliUpdateUseCase { + private readonly cliUpdateCheckPort; + constructor(cliUpdateCheckPort: CliUpdateCheckPort); + execute(installedVersion: string): Promise; +} diff --git a/build/cli/src/cli/cli_program.d.ts b/build/cli/src/cli/cli_program.d.ts index 4a395de1..544ee1d1 100644 --- a/build/cli/src/cli/cli_program.d.ts +++ b/build/cli/src/cli/cli_program.d.ts @@ -1,2 +1,3 @@ import { Command } from 'commander'; -export declare function createCliProgram(): Command; +import { type CliUpdateChecker } from './cli_update_notification'; +export declare function createCliProgram(updateChecker?: CliUpdateChecker): Command; diff --git a/build/cli/src/cli/cli_update_check_policy.d.ts b/build/cli/src/cli/cli_update_check_policy.d.ts new file mode 100644 index 00000000..983127fe --- /dev/null +++ b/build/cli/src/cli/cli_update_check_policy.d.ts @@ -0,0 +1,3 @@ +export declare const UPDATE_CHECK_DISABLED_ENV = "COPILOT_DISABLE_UPDATE_CHECK"; +export declare function isUpdateCheckDisabled(environment?: NodeJS.ProcessEnv): boolean; +export declare function shouldCheckForUpdates(commandName: string): boolean; diff --git a/build/cli/src/cli/cli_update_notification.d.ts b/build/cli/src/cli/cli_update_notification.d.ts new file mode 100644 index 00000000..826489f8 --- /dev/null +++ b/build/cli/src/cli/cli_update_notification.d.ts @@ -0,0 +1,6 @@ +import type { CliUpdateAvailable } from '../application/usecases/check_cli_update_use_case'; +export interface CliUpdateChecker { + execute(installedVersion: string): Promise; +} +/** Displays advisory update information while keeping update failures invisible to users. */ +export declare function notifyAboutCliUpdate(checker: CliUpdateChecker, installedVersion: string, output?: Pick): Promise; diff --git a/build/cli/src/domain/cli_version.d.ts b/build/cli/src/domain/cli_version.d.ts new file mode 100644 index 00000000..e315ca2d --- /dev/null +++ b/build/cli/src/domain/cli_version.d.ts @@ -0,0 +1,4 @@ +/** Compares two CLI versions using release and prerelease precedence. */ +export declare function compareCliVersions(left: string, right: string): number | undefined; +/** Returns true only when the published version is newer than the installed one. */ +export declare function isNewerCliVersion(installedVersion: string, publishedVersion: string): boolean; diff --git a/build/cli/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts b/build/cli/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts new file mode 100644 index 00000000..faae0617 --- /dev/null +++ b/build/cli/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts @@ -0,0 +1,37 @@ +import type { CliUpdateCheckPort } from '../../application/ports/cli_update_check_ports'; +export declare const NPM_REGISTRY_URL: string; +export declare const UPDATE_CHECK_CACHE_TTL_MS: number; +export declare const UPDATE_CHECK_TIMEOUT_MS = 1500; +export interface UpdateCheckCacheEntry { + checkedAt: number; + latestVersion?: string; +} +export interface CliUpdateCheckCache { + read(): UpdateCheckCacheEntry | undefined; + write(entry: UpdateCheckCacheEntry): void; +} +export declare function resolveUpdateCheckCachePath(platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv, homeDirectory?: string): string; +export declare class FileCliUpdateCheckCache implements CliUpdateCheckCache { + private readonly filePath; + constructor(filePath?: string); + read(): UpdateCheckCacheEntry | undefined; + write(entry: UpdateCheckCacheEntry): void; +} +export interface NpmCliUpdateCheckAdapterOptions { + cache?: CliUpdateCheckCache; + fetcher?: typeof fetch; + now?: () => number; + cacheTtlMs?: number; + timeoutMs?: number; +} +/** Reads npm's latest dist-tag with bounded latency and a non-sensitive local cache. */ +export declare class NpmCliUpdateCheckAdapter implements CliUpdateCheckPort { + private readonly cache; + private readonly fetcher; + private readonly now; + private readonly cacheTtlMs; + private readonly timeoutMs; + constructor(options?: NpmCliUpdateCheckAdapterOptions); + getLatestPublishedVersion(): Promise; + private writeCache; +} diff --git a/build/cli/src/infrastructure/composition/cli_update_check_composition_root.d.ts b/build/cli/src/infrastructure/composition/cli_update_check_composition_root.d.ts new file mode 100644 index 00000000..7b9f318b --- /dev/null +++ b/build/cli/src/infrastructure/composition/cli_update_check_composition_root.d.ts @@ -0,0 +1,2 @@ +import { CheckCliUpdateUseCase } from '../../application/usecases/check_cli_update_use_case'; +export declare function createCliUpdateCheckUseCase(): CheckCliUpdateUseCase; diff --git a/build/github_action/src/application/ports/cli_update_check_ports.d.ts b/build/github_action/src/application/ports/cli_update_check_ports.d.ts new file mode 100644 index 00000000..908085f5 --- /dev/null +++ b/build/github_action/src/application/ports/cli_update_check_ports.d.ts @@ -0,0 +1,4 @@ +/** Retrieves the latest published version of the Copilot CLI. */ +export interface CliUpdateCheckPort { + getLatestPublishedVersion(): Promise; +} diff --git a/build/github_action/src/application/usecases/check_cli_update_use_case.d.ts b/build/github_action/src/application/usecases/check_cli_update_use_case.d.ts new file mode 100644 index 00000000..5bbe9290 --- /dev/null +++ b/build/github_action/src/application/usecases/check_cli_update_use_case.d.ts @@ -0,0 +1,11 @@ +import type { CliUpdateCheckPort } from '../ports/cli_update_check_ports'; +export interface CliUpdateAvailable { + installedVersion: string; + publishedVersion: string; +} +/** Checks for a newer published CLI version without coupling the application to npm. */ +export declare class CheckCliUpdateUseCase { + private readonly cliUpdateCheckPort; + constructor(cliUpdateCheckPort: CliUpdateCheckPort); + execute(installedVersion: string): Promise; +} diff --git a/build/github_action/src/cli/cli_program.d.ts b/build/github_action/src/cli/cli_program.d.ts index 4a395de1..544ee1d1 100644 --- a/build/github_action/src/cli/cli_program.d.ts +++ b/build/github_action/src/cli/cli_program.d.ts @@ -1,2 +1,3 @@ import { Command } from 'commander'; -export declare function createCliProgram(): Command; +import { type CliUpdateChecker } from './cli_update_notification'; +export declare function createCliProgram(updateChecker?: CliUpdateChecker): Command; diff --git a/build/github_action/src/cli/cli_update_check_policy.d.ts b/build/github_action/src/cli/cli_update_check_policy.d.ts new file mode 100644 index 00000000..983127fe --- /dev/null +++ b/build/github_action/src/cli/cli_update_check_policy.d.ts @@ -0,0 +1,3 @@ +export declare const UPDATE_CHECK_DISABLED_ENV = "COPILOT_DISABLE_UPDATE_CHECK"; +export declare function isUpdateCheckDisabled(environment?: NodeJS.ProcessEnv): boolean; +export declare function shouldCheckForUpdates(commandName: string): boolean; diff --git a/build/github_action/src/cli/cli_update_notification.d.ts b/build/github_action/src/cli/cli_update_notification.d.ts new file mode 100644 index 00000000..826489f8 --- /dev/null +++ b/build/github_action/src/cli/cli_update_notification.d.ts @@ -0,0 +1,6 @@ +import type { CliUpdateAvailable } from '../application/usecases/check_cli_update_use_case'; +export interface CliUpdateChecker { + execute(installedVersion: string): Promise; +} +/** Displays advisory update information while keeping update failures invisible to users. */ +export declare function notifyAboutCliUpdate(checker: CliUpdateChecker, installedVersion: string, output?: Pick): Promise; diff --git a/build/github_action/src/domain/cli_version.d.ts b/build/github_action/src/domain/cli_version.d.ts new file mode 100644 index 00000000..e315ca2d --- /dev/null +++ b/build/github_action/src/domain/cli_version.d.ts @@ -0,0 +1,4 @@ +/** Compares two CLI versions using release and prerelease precedence. */ +export declare function compareCliVersions(left: string, right: string): number | undefined; +/** Returns true only when the published version is newer than the installed one. */ +export declare function isNewerCliVersion(installedVersion: string, publishedVersion: string): boolean; diff --git a/build/github_action/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts b/build/github_action/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts new file mode 100644 index 00000000..faae0617 --- /dev/null +++ b/build/github_action/src/infrastructure/cli/npm_cli_update_check_adapter.d.ts @@ -0,0 +1,37 @@ +import type { CliUpdateCheckPort } from '../../application/ports/cli_update_check_ports'; +export declare const NPM_REGISTRY_URL: string; +export declare const UPDATE_CHECK_CACHE_TTL_MS: number; +export declare const UPDATE_CHECK_TIMEOUT_MS = 1500; +export interface UpdateCheckCacheEntry { + checkedAt: number; + latestVersion?: string; +} +export interface CliUpdateCheckCache { + read(): UpdateCheckCacheEntry | undefined; + write(entry: UpdateCheckCacheEntry): void; +} +export declare function resolveUpdateCheckCachePath(platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv, homeDirectory?: string): string; +export declare class FileCliUpdateCheckCache implements CliUpdateCheckCache { + private readonly filePath; + constructor(filePath?: string); + read(): UpdateCheckCacheEntry | undefined; + write(entry: UpdateCheckCacheEntry): void; +} +export interface NpmCliUpdateCheckAdapterOptions { + cache?: CliUpdateCheckCache; + fetcher?: typeof fetch; + now?: () => number; + cacheTtlMs?: number; + timeoutMs?: number; +} +/** Reads npm's latest dist-tag with bounded latency and a non-sensitive local cache. */ +export declare class NpmCliUpdateCheckAdapter implements CliUpdateCheckPort { + private readonly cache; + private readonly fetcher; + private readonly now; + private readonly cacheTtlMs; + private readonly timeoutMs; + constructor(options?: NpmCliUpdateCheckAdapterOptions); + getLatestPublishedVersion(): Promise; + private writeCache; +} diff --git a/build/github_action/src/infrastructure/composition/cli_update_check_composition_root.d.ts b/build/github_action/src/infrastructure/composition/cli_update_check_composition_root.d.ts new file mode 100644 index 00000000..7b9f318b --- /dev/null +++ b/build/github_action/src/infrastructure/composition/cli_update_check_composition_root.d.ts @@ -0,0 +1,2 @@ +import { CheckCliUpdateUseCase } from '../../application/usecases/check_cli_update_use_case'; +export declare function createCliUpdateCheckUseCase(): CheckCliUpdateUseCase; diff --git a/docs.json b/docs.json index a2052144..6661fafc 100644 --- a/docs.json +++ b/docs.json @@ -78,6 +78,16 @@ "href": "/how-to-use", "icon": "list-check" }, + { + "title": "Authentication", + "href": "/authentication", + "icon": "key" + }, + { + "title": "Configuration", + "href": "/configuration", + "icon": "gear" + }, { "title": "Features & Capabilities", "href": "/features", @@ -185,6 +195,11 @@ "href": "/security-operations/security/secret-exposure", "icon": "lock" }, + { + "title": "Prompt injection", + "href": "/security-operations/security/prompt-injection", + "icon": "triangle-exclamation" + }, { "title": "Authentication compliance", "href": "/security-operations/security/authentication-compliance", @@ -436,6 +451,11 @@ "href": "/single-actions/permissions", "icon": "shield" }, + { + "title": "Deploy label and merge", + "href": "/single-actions/deploy-label-and-merge", + "icon": "git-merge" + }, { "title": "Examples", "href": "/single-actions/examples", diff --git a/docs/agents/cli-configuration.mdx b/docs/agents/cli-configuration.mdx index 8c980e00..f7d39b0b 100644 --- a/docs/agents/cli-configuration.mdx +++ b/docs/agents/cli-configuration.mdx @@ -56,7 +56,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} steps: - uses: actions/checkout@v4 - - uses: vypdev/copilot@master + - uses: vypdev/copilot@v3 with: token: ${{ secrets.GITHUB_TOKEN }} agent-provider: ${{ env.AGENT_PROVIDER }} diff --git a/docs/agents/codex-openai.mdx b/docs/agents/codex-openai.mdx index ebae8418..e8bd6a79 100644 --- a/docs/agents/codex-openai.mdx +++ b/docs/agents/codex-openai.mdx @@ -54,7 +54,7 @@ CODEX_VERSION= The approved package installation shape is: ```bash -corepack pnpm add --global "@openai/codex@${CODEX_VERSION}" +npm install --global "@openai/codex@${CODEX_VERSION}" ``` Verify the executable before a real request: diff --git a/docs/agents/opencode.mdx b/docs/agents/opencode.mdx index c19965e7..6dc97103 100644 --- a/docs/agents/opencode.mdx +++ b/docs/agents/opencode.mdx @@ -35,14 +35,14 @@ For CI, provide the selected model provider's credential through a GitHub Secret Do not put API keys in action inputs, command arguments, prompts, repository files, or logs. A local OAuth login may work on a controlled self-hosted runner, but local authentication files must not be copied into GitHub-hosted CI. -OpenCode supports many providers and local models. That flexibility does not change the Bugbot contract: the effective provider/model must be explicitly selected and, when configured, must pass the allowlists before the CLI starts. +OpenCode supports many providers and local models. That flexibility does not change the Copilot agent contract: the effective provider/model must be explicitly selected and, when configured, must pass the allowlists before the CLI starts. ## Runtime and provisioning The Action can provision only the selected runtime in GitHub Actions using pinned version variables. OpenCode is installed as: ```bash -corepack pnpm add --global "opencode-ai@${OPENCODE_VERSION}" +npm install --global "opencode-ai@${OPENCODE_VERSION}" ``` The binary must pass version and headless-help checks before use: @@ -79,7 +79,7 @@ The expected evidence is `READY` and exit code `0`. A successful binary/help che ### Missing executable -Install the pinned OpenCode version with Corepack and pnpm, then rerun: +Install the pinned OpenCode version with the runner's `npm`, then rerun: ```bash command -v opencode diff --git a/docs/authentication.mdx b/docs/authentication.mdx index 16b42867..9f453b83 100644 --- a/docs/authentication.mdx +++ b/docs/authentication.mdx @@ -106,7 +106,7 @@ The setup PAT and workflow PAT may have different owners and permissions. Do not name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: project-ids: 1,2 token: ${{ secrets.PAT }} diff --git a/docs/bugbot/examples.mdx b/docs/bugbot/examples.mdx index 6c6da028..3ac33a4b 100644 --- a/docs/bugbot/examples.mdx +++ b/docs/bugbot/examples.mdx @@ -31,7 +31,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: ${{ vars.PROJECT_IDS }} @@ -69,7 +69,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} @@ -104,10 +104,10 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} + agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} ai-ignore-files: build/* @@ -145,7 +145,7 @@ jobs: with: ref: ${{ github.event.inputs.branch || format('feature/issue-{0}', github.event.inputs.issue_number) }} - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: detect_potential_problems_action diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 2d60db61..64e2bf8a 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -130,6 +130,82 @@ Copilot provides extensive configuration options to customize your workflow. Use +## Complete input reference + +The tables above group the most commonly changed inputs. The following inputs are +also part of the public `action.yml` contract and are easy to miss when copying a +workflow. Defaults below are the action defaults; values supplied by `copilot setup` +may be forwarded through Repository Variables instead. + +### Operational and single-action inputs + +| Input | Default | Description | +| --- | --- | --- | +| `debug` | `false` | Enable verbose diagnostic logging. Do not enable it when credential-bearing values could be exposed by surrounding steps. | +| `single-action` | empty | Run one explicit action instead of the event-driven pipeline. | +| `single-action-issue` | empty | Issue number for a single action that operates on an issue. | +| `single-action-version` | empty | Version used by release and tag single actions. | +| `single-action-title` | empty | Title used by `create_release`. | +| `single-action-changelog` | empty | Markdown body used by `create_release`. | +| `queue-gate-only` | `false` | Internal control-plane mode used by the release and hotfix setup workflows to admit a run before mutation work. Do not use it as a replacement for a normal action invocation. | + +### Task-specific agent overrides + +The common `agent-*` inputs define the baseline runtime/model tuple. `findings-*` +and `fixer-*` override that tuple for Bugbot analysis and autofix. The other roles +are optional overrides: when at least one role-specific value is supplied, that +role gets its own validated configuration; otherwise it inherits the common tuple. + +| Role | Provider | Model provider | Model | Effort | Command | +| --- | --- | --- | --- | --- | --- | +| Planner | `planner-provider` | `planner-model-provider` | `planner-model` | `planner-effort` | `planner-command` | +| Findings | `findings-provider` | `findings-model-provider` | `findings-model` | `findings-effort` | `findings-command` | +| Reviewer | `reviewer-provider` | `reviewer-model-provider` | `reviewer-model` | `reviewer-effort` | `reviewer-command` | +| Fixer | `fixer-provider` | `fixer-model-provider` | `fixer-model` | `fixer-effort` | `fixer-command` | +| Tester | `tester-provider` | `tester-model-provider` | `tester-model` | `tester-effort` | `tester-command` | +| Release | `release-provider` | `release-model-provider` | `release-model` | `release-effort` | `release-command` | + +`copilot setup` can configure the provider, model provider, model, and effort for +these six roles independently. The setup workflow templates also accept the +corresponding `*_COMMAND` variables when you deliberately define a custom command; +custom commands are executable configuration and are not inferred automatically. + +### Lifecycle labels + +These inputs configure the labels synchronized by issue, pull request, and agent +activity flows. The durable lifecycle phase is exclusive; the temporary activity +and waiting dimensions can coexist with it. + +| Input | Default | Meaning | +| --- | --- | --- | +| `state-planned-label` | `state:planned` | A plan is available. | +| `state-in-progress-label` | `state:in-progress` | Implementation is in progress. | +| `state-reviewing-label` | `state:reviewing` | A pull request is under review. | +| `state-changes-requested-label` | `state:changes-requested` | Review findings require changes. | +| `state-verified-label` | `state:verified` | The pull request was merged successfully. | +| `state-ready-label` | `state:ready` | The latest result is ready for human action. | +| `state-blocked-label` | `state:blocked` | Human intervention is required. | +| `state-ai-processing-label` | `state:ai-processing` | Temporary marker while an agent is analyzing or working; removed at run end. | +| `state-awaiting-maintainer-label` | `state:awaiting-maintainer` | Waiting for a maintainer response, approval, or merge. | +| `state-awaiting-issue-author-label` | `state:awaiting-issue-author` | Waiting for information or changes from the issue author. | + +### Issue type inputs + +`copilot setup` uses these inputs when creating or updating organization Issue +Types. Each type has a configurable name, description, and color. + +| Type | Name | Description | Color | Defaults (name / color) | +| --- | --- | --- | --- | --- | +| Task | `issue-type-task` | `issue-type-task-description` | `issue-type-task-color` | `Task` / `blue` | +| Bug | `issue-type-bug` | `issue-type-bug-description` | `issue-type-bug-color` | `Bug` / `orange` | +| Feature | `issue-type-feature` | `issue-type-feature-description` | `issue-type-feature-color` | `Feature` / `green` | +| Documentation | `issue-type-documentation` | `issue-type-documentation-description` | `issue-type-documentation-color` | `Documentation` / `pink` | +| Maintenance | `issue-type-maintenance` | `issue-type-maintenance-description` | `issue-type-maintenance-color` | `Maintenance` / `purple` | +| Hotfix | `issue-type-hotfix` | `issue-type-hotfix-description` | `issue-type-hotfix-color` | `Hotfix` / `red` | +| Release | `issue-type-release` | `issue-type-release-description` | `issue-type-release-color` | `Release` / `yellow` | +| Question | `issue-type-question` | `issue-type-question-description` | `issue-type-question-color` | `Question` / `gray` | +| Help | `issue-type-help` | `issue-type-help-description` | `issue-type-help-color` | `Help` / `purple` | + ## Persistence & Merging Copilot persists certain configuration fields directly in the **GitHub Issue description** as a hidden JSON block. This allows the action to "remember" state across different runs and across issues/PRs. diff --git a/docs/development/documentation-completeness-plan.mdx b/docs/development/documentation-completeness-plan.mdx index c6d3ea3b..f837d90b 100644 --- a/docs/development/documentation-completeness-plan.mdx +++ b/docs/development/documentation-completeness-plan.mdx @@ -27,4 +27,8 @@ Every public page MUST: - use MDX and appear in `docs.json` when it is a user-facing reference; - avoid secrets, authentication values, and unverifiable success claims. -The repository validator checks action inputs, defaults, allowlists, workflow references, navigation routes, and forbidden legacy identifiers. A documentation change is incomplete until the validator, tests, lint, build, and diff checks pass. +The repository validators check action inputs, defaults, allowlists, workflow references, +navigation routes, local links, YAML snippets, Action version references, and forbidden +legacy identifiers. Run `corepack pnpm run validate:documentation` for the cross-page +contract check. A documentation change is incomplete until the documentation validators, +tests, lint, build, and diff checks pass. diff --git a/docs/development/documentation.mdx b/docs/development/documentation.mdx index cae57b33..db4a6c77 100644 --- a/docs/development/documentation.mdx +++ b/docs/development/documentation.mdx @@ -14,14 +14,15 @@ Documentation is part of the product contract. A page is complete only when its - `.github/workflows/`: event, permissions, and variable propagation. - `docs.json`: tabs, groups, titles, icons, and routes. - `docs-page` catalog: supported MDX components and icon vocabulary. +- `scripts/validate-documentation-contract.cjs`: cross-page links, YAML snippets, Action refs, and input coverage. ## Change procedure Read the implementation and workflow before changing prose. Keep one user task or concept per page. - Use only supported MDX components and icons. - Check links, frontmatter, defaults, terms, and generated artifacts. + Use only supported MDX components and icons, and register every public page in `docs.json`. + Run `validate:agent-docs`, `validate:docs-page`, and `validate:documentation` to check links, YAML snippets, frontmatter, defaults, action refs, terms, and input coverage. See the [documentation completeness plan](/development/documentation-completeness-plan) for the complete information architecture. diff --git a/docs/development/release-process.mdx b/docs/development/release-process.mdx index 38c17176..0de6fee8 100644 --- a/docs/development/release-process.mdx +++ b/docs/development/release-process.mdx @@ -23,6 +23,7 @@ Before publishing locally, validate the package contents: corepack pnpm run build corepack pnpm run validate:npm-package corepack pnpm run smoke:npm-package +corepack pnpm run validate:documentation ``` The internal release and hotfix workflows create the GitHub release. The separate [`publish_npm.yml`](https://github.com/vypdev/copilot/blob/master/.github/workflows/publish_npm.yml) workflow then publishes the exact release tag with npm trusted publishing on a GitHub-hosted runner: diff --git a/docs/development/testing.mdx b/docs/development/testing.mdx index ceea8ee8..1ef73921 100644 --- a/docs/development/testing.mdx +++ b/docs/development/testing.mdx @@ -13,6 +13,7 @@ corepack pnpm@10.12.4 run test:coverage corepack pnpm@10.12.4 run build corepack pnpm@10.12.4 run validate:agent-docs corepack pnpm@10.12.4 run validate:docs-page +corepack pnpm@10.12.4 run validate:documentation corepack pnpm@10.12.4 run validate:workflows git diff --check ``` diff --git a/docs/features.mdx b/docs/features.mdx index a0500aa1..8812e38a 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -85,7 +85,7 @@ When you set `single-action` (and, when required, `single-action-issue`, `single | **`initial_setup`** | — | Performs initial setup: creates labels, issue types, verifies access. If the repo has no version tags, creates default tag `v1.0.0` so release/hotfix issues get a base version. No issue required. | | **`create_release`** | `single-action-version`, `single-action-title`, `single-action-changelog` | Creates a GitHub release with the given version, title, and changelog. | | **`create_tag`** | `single-action-version` | Creates a Git tag with prefix `v` (e.g. `v1.2.0`) for the given version from the release branch. | -| **`publish_github_action`** | `single-action-version` | Publishes or updates the GitHub Action: creates/updates the major version tag (e.g. `v2` from `v2.0.4`). Requires `create_tag` to have been run first. | +| **`publish_github_action`** | `single-action-version` | Publishes or updates the GitHub Action: creates/updates the major version tag (for example, `v3` from a `v3.x.y` release). Requires `create_tag` to have been run first. | | **`deployed_action`** | `single-action-issue` | Marks the issue as deployed; updates labels and project state (e.g. "deployed"). | Single actions that **throw an error** if the last step fails: `publish_github_action`, `create_release`, `deployed_action`, `create_tag`. This lets the workflow fail the job when the action does not succeed. @@ -174,7 +174,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: vypdev/copilot@master + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: '2,3' diff --git a/docs/how-to-use.mdx b/docs/how-to-use.mdx index 2488de40..56a0bcdc 100644 --- a/docs/how-to-use.mdx +++ b/docs/how-to-use.mdx @@ -18,7 +18,9 @@ Use Node.js 24 or newer. You can check the active version before installing: node --version ``` -Run the command from the root of the repository where you want Copilot to operate, not from the package installation directory. +Run repository-dependent commands from the root of the repository where you want +Copilot to operate, not from the package installation directory. Version and help +commands can be run from any directory. For local development or when testing an unreleased checkout, install from the repository instead: @@ -30,7 +32,7 @@ corepack pnpm install . --global If the checkout does not include the compiled `build/` folder (e.g. it is gitignored), run `corepack pnpm install` and `corepack pnpm run build` before `corepack pnpm install . --global`. -Once installed, the `copilot` command is available globally. **All Copilot CLI commands** (including `copilot setup`, `copilot doctor`, and `copilot check-progress -i 123`) must be run **from inside the repository** where you want Copilot to run. Commands that access GitHub accept `--token` or `PERSONAL_ACCESS_TOKEN` from the environment. `copilot setup` and `copilot doctor` securely prompt for the setup PAT when run interactively; no `.env` file is read or created. `copilot setup --dry-run` is the only setup mode that can run without a token. See [CLI commands](/single-actions/workflow-and-cli). +Once installed, the `copilot` command is available globally. Repository-dependent commands such as `copilot setup`, `copilot doctor`, `copilot check-progress`, `copilot think`, and `copilot do` must be run **from the root of the target repository**. The `copilot upgrade`, `copilot --version`, and help flows can run from any directory. Commands that access GitHub accept `--token` or `PERSONAL_ACCESS_TOKEN` from the environment. `copilot setup` and `copilot doctor` securely prompt for the setup PAT when run interactively; no `.env` file is read or created. `copilot setup --dry-run` is the only setup mode that can run without a token. See [CLI commands](/single-actions/workflow-and-cli). If you previously installed Copilot from a local checkout, installing the npm package switches the same `copilot` command to the published package. Check which executable and package are active: @@ -59,7 +61,9 @@ copilot upgrade copilot --version ``` -`copilot upgrade` can be run from any directory; it updates the published npm installation and does not require a GitHub repository or `PERSONAL_ACCESS_TOKEN`. If you are using a version older than `3.3.0`, update it once with `npm install --global @vypdev/copilot@latest`, then use `copilot upgrade` for future updates. +`copilot upgrade` can be run from any directory; it updates the published npm installation and does not require a GitHub repository or `PERSONAL_ACCESS_TOKEN`. If the installed version does not include the `upgrade` command, bootstrap it once with `npm install --global @vypdev/copilot@latest`, then use `copilot upgrade` for future updates. + +Before running a normal CLI command, Copilot performs a lightweight, informational check for a newer published version. The check uses a local 24-hour cache, has a short timeout, never blocks or changes the command result, and stays silent when npm is unavailable. If an update is found, Copilot prints `A new version (x.y.z) is available. Run "copilot upgrade".` The `upgrade`, `--version`, and help flows do not trigger this check. To disable it, set `COPILOT_DISABLE_UPDATE_CHECK=1`. For an unreleased checkout, update the checkout and reinstall the local package globally: @@ -142,7 +146,7 @@ The complete command reference, including every supported option, is in [Workflo - **Branch name prefixes**: The action uses inputs like `feature-tree`, `bugfix-tree`, `release-tree`, `hotfix-tree` (defaults: `feature`, `bugfix`, `release`, `hotfix`) to create branch names. If you change them, branch names will follow the new prefixes; keep templates and docs in sync. - **Project columns**: Default column names are "Todo" and "In Progress". If you rename columns in GitHub Projects, set the corresponding action inputs (`project-column-issue-created`, `project-column-issue-in-progress`, etc.) so the action moves issues/PRs to the correct columns. - **Bugbot autofix (issue/PR comments):** Workflows that run on `issue_comment` or `pull_request_review_comment` (so users can ask the bot to fix reported findings) must grant **`contents: write`** so the action can commit and push. On **issue_comment**, the action resolves the branch from an open PR that references the issue and checks out that branch before applying fixes and pushing. See [OpenCode → How Bugbot works](/agents/opencode#how-bugbot-works-potential-problems) and [Troubleshooting → Bugbot autofix](/security-operations/operations/troubleshooting#bugbot-autofix). + **Bugbot autofix (issue/PR comments):** Workflows that run on `issue_comment` or `pull_request_review_comment` (so users can ask the bot to fix reported findings) must grant **`contents: write`** so the action can commit and push. On **issue_comment**, the action resolves the branch from an open PR that references the issue and checks out that branch before applying fixes and pushing. See [Bugbot autofix](/bugbot/autofix) and [Troubleshooting → Bugbot autofix](/security-operations/operations/troubleshooting#bugbot-autofix). @@ -281,9 +285,26 @@ All `.yml` / `.yaml` files here are copied to `.github/workflows/`. Default file | `copilot_pull_request_comment.yml` | Runs on PR review comment: e.g. translation checks. | | `release_workflow.yml` | **Manual** (`workflow_dispatch`): release flow (version, changelog, tag, release, deployed). Filename must match the action input `release-workflow` (default: `release_workflow.yml`) so the Issue workflow can dispatch it when the **deploy** label is added. | | `hotfix_workflow.yml` | **Manual** (`workflow_dispatch`): hotfix flow. Filename must match the action input `hotfix-workflow` (default: `hotfix_workflow.yml`). | +| `agent-cli-provisioning.yml` | **Manual** (`workflow_dispatch`): verifies that the selected agent runtimes are available, pinned, and usable on the runner. | +| `copilot_credential_health.yml` | **Manual** (`workflow_dispatch`): validates selected remote credentials without exposing their values; used by `copilot doctor`. | Each Copilot workflow step passes at least `token` and the configured agent CLI inputs via `vars.*`. The **release** and **hotfix** workflows are **dispatched by the action** when an issue has the deploy label and the corresponding release/hotfix context (branch, version, etc.); they are not triggered by issue events directly. +The `agent-cli-provisioning.yml` workflow checks the selected runtime binaries and +their pinned provisioning inputs. The `copilot_credential_health.yml` workflow is +read-only with respect to repository configuration: it executes provider-specific +health checks and reports only whether each requested credential is usable. GitHub +does not expose Secret values through its API, so `copilot doctor` dispatches this +workflow when it is available on the repository's default branch. + +When Repository Variables are enabled, setup creates the common `AGENT_*` contract, +the provider/model/effort variables for each configured `FINDINGS_*`, `FIXER_*`, +`PLANNER_*`, `REVIEWER_*`, `TESTER_*`, and `RELEASE_*` role, the repository and +project variables, and the AI/Bugbot variables. The workflow templates read +`*_COMMAND` pass-through variables too, but custom commands are executable +configuration and must be reviewed and added deliberately; setup does not invent +them from a model selection. + ### `setup/ISSUE_TEMPLATE/` All files here are copied to `.github/ISSUE_TEMPLATE/`. @@ -305,17 +326,13 @@ The **labels** in each template must match the label names configured in the act Copied to `.github/pull_request_template.md`. Used as the default body for new PRs. The AI PR description feature can fill this structure; you can edit the sections (Summary, Related Issues, Scope, Technical Details, How to Test, etc.) to fit your repo. No Copilot logic depends on specific headings; only the deploy/release/hotfix flows depend on **workflow filenames** and **label names**. -### `setup/workflows/copilot_credential_health.yml` - -This manual workflow verifies selected remote credentials without printing or returning their values. `copilot doctor` dispatches it when the workflow is present on the repository's default branch. GitHub only exposes Secret names through its API, so this workflow is required to verify the value of an existing Secret. - --- ## Optional: Configure projects, AI, and workflows -After the tutorial and file adaption, you can: +After the tutorial and file customization, you can: -- Set **repository or organization variables** for the agent CLI contract (`AGENT_PROVIDER`, `AGENT_MODEL_PROVIDER`, `AGENT_MODEL`, `AGENT_EFFORT`, `AGENT_ALLOWED_MODEL_PROVIDERS`, and `AGENT_ALLOWED_MODELS`) and, when needed, independent `FINDINGS_*` and `FIXER_*` overrides. The supplied Copilot workflow templates forward these values to the action. Keep `CURSOR_API_KEY` available only when the common, findings, or fixer provider is Cursor. +- Set **repository or organization variables** for the agent CLI contract (`AGENT_PROVIDER`, `AGENT_MODEL_PROVIDER`, `AGENT_MODEL`, `AGENT_EFFORT`, `AGENT_ALLOWED_MODEL_PROVIDERS`, and `AGENT_ALLOWED_MODELS`) and, when needed, independent task overrides for `FINDINGS_*`, `REVIEWER_*`, `PLANNER_*`, `FIXER_*`, `TESTER_*`, and `RELEASE_*`. The supplied Copilot workflow templates forward these values to the action. Keep `CURSOR_API_KEY` available only when one of the configured task providers is Cursor. - Adjust **project column names** and **branch names** via action inputs so the action moves issues/PRs to the right columns and uses your branch naming. - Customize **issue templates** (copy, add fields, change labels) while keeping label and workflow names consistent as above. - Add or modify **release/hotfix** workflow steps (e.g. build, deploy) while keeping the workflow **filenames** and the action inputs `release-workflow` and `hotfix-workflow` in sync. diff --git a/docs/issues/assignees-and-projects.mdx b/docs/issues/assignees-and-projects.mdx index 4b50c4b8..a879a286 100644 --- a/docs/issues/assignees-and-projects.mdx +++ b/docs/issues/assignees-and-projects.mdx @@ -21,7 +21,7 @@ When the action runs on an issue (e.g. opened or labeled), it can assign **up to Set the number of assignees in the workflow: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: ${{ vars.PROJECT_IDS }} @@ -44,7 +44,7 @@ Linking issues to **GitHub Project** boards requires a **Personal Access Token ( You can link each issue to **multiple** boards by listing several IDs: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: '1,2,3' diff --git a/docs/issues/branch-management.mdx b/docs/issues/branch-management.mdx index c371313b..3186276b 100644 --- a/docs/issues/branch-management.mdx +++ b/docs/issues/branch-management.mdx @@ -21,7 +21,7 @@ For **feature**, **bugfix**, **docs**, and **chore** issues, the action does **n Workflow: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} branch-management-launcher-label: branched @@ -32,7 +32,7 @@ Flow: Open issue with label `feature` → no branch yet. Add label `branched` ### Example: create branch without launcher ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} branch-management-always: true @@ -67,7 +67,7 @@ Use **`commit-prefix-transforms`** (e.g. `replace-slash`) so commit message pref ### Example: custom naming ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} main-branch: main diff --git a/docs/issues/examples.mdx b/docs/issues/examples.mdx index fda9bf41..f000a484 100644 --- a/docs/issues/examples.mdx +++ b/docs/issues/examples.mdx @@ -26,7 +26,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: ${{ vars.PROJECT_IDS }} @@ -57,7 +57,7 @@ jobs: Create branches as soon as the issue has a type label (no launcher label): ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: ${{ vars.PROJECT_IDS }} @@ -101,7 +101,7 @@ Your `.github/workflows/` must contain files with these exact names (or pass the ```yaml # In copilot_issue.yml (or wherever you call Copilot for issues) -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} release-workflow: release_workflow.yml diff --git a/docs/issues/type/bugfix.mdx b/docs/issues/type/bugfix.mdx index dbb9a4a4..d9843b6f 100644 --- a/docs/issues/type/bugfix.mdx +++ b/docs/issues/type/bugfix.mdx @@ -15,9 +15,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - bugfix-label: bugfix // [!code highlight] + bugfix-label: bugfix # [!code highlight] ``` ## Naming @@ -30,9 +30,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - bugfix-tree: bugfix // [!code highlight] + bugfix-tree: bugfix # [!code highlight] ``` Bugfix branches follow this naming convention: @@ -56,9 +56,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - development-branch: develop // [!code highlight] + development-branch: develop # [!code highlight] ``` ## Images @@ -73,9 +73,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - images-issue-bugfix: url1, url2, url3 // [!code highlight] + images-issue-bugfix: url1, url2, url3 # [!code highlight] ``` diff --git a/docs/issues/type/chore.mdx b/docs/issues/type/chore.mdx index 97790911..f07cd462 100644 --- a/docs/issues/type/chore.mdx +++ b/docs/issues/type/chore.mdx @@ -15,9 +15,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - chore-label: chore // [!code highlight] + chore-label: chore # [!code highlight] ``` ## Naming @@ -30,9 +30,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - chore-tree: chore // [!code highlight] + chore-tree: chore # [!code highlight] ``` Chore branches follow this naming convention: @@ -56,9 +56,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - development-branch: develop // [!code highlight] + development-branch: develop # [!code highlight] ``` ## Images @@ -73,9 +73,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - images-issue-chore: url1, url2, url3 // [!code highlight] + images-issue-chore: url1, url2, url3 # [!code highlight] ``` diff --git a/docs/issues/type/docs.mdx b/docs/issues/type/docs.mdx index 577853a8..a6e9169e 100644 --- a/docs/issues/type/docs.mdx +++ b/docs/issues/type/docs.mdx @@ -15,9 +15,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - docs-label: docs // [!code highlight] + docs-label: docs # [!code highlight] ``` ## Naming @@ -30,9 +30,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - docs-tree: docs // [!code highlight] + docs-tree: docs # [!code highlight] ``` Documentation branches follow this naming convention: @@ -56,9 +56,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - development-branch: develop // [!code highlight] + development-branch: develop # [!code highlight] ``` ## Images @@ -73,9 +73,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - images-issue-docs: url1, url2, url3 // [!code highlight] + images-issue-docs: url1, url2, url3 # [!code highlight] ``` @@ -96,7 +96,7 @@ You can find this template in `.github/ISSUE_TEMPLATE/doc_update.yml`. Below is name: 📝 Documentation Update description: Propose changes or improvements to the documentation title: "" -labels: ["documentation", "docs"] // [!code highlight] +labels: ["documentation", "docs"] # [!code highlight] body: - type: checkboxes attributes: diff --git a/docs/issues/type/feature.mdx b/docs/issues/type/feature.mdx index b1c97ed0..92a76ede 100644 --- a/docs/issues/type/feature.mdx +++ b/docs/issues/type/feature.mdx @@ -15,9 +15,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - feature-label: feature // [!code highlight] + feature-label: feature # [!code highlight] ``` ## Naming @@ -30,9 +30,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - feature-tree: feature // [!code highlight] + feature-tree: feature # [!code highlight] ``` Feature branches follow this naming convention: @@ -56,9 +56,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - development-branch: develop // [!code highlight] + development-branch: develop # [!code highlight] ``` ## Images @@ -73,9 +73,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - images-issue-feature: url1, url2, url3 // [!code highlight] + images-issue-feature: url1, url2, url3 # [!code highlight] ``` diff --git a/docs/issues/type/hotfix.mdx b/docs/issues/type/hotfix.mdx index 9c50b2ba..3a426cd3 100644 --- a/docs/issues/type/hotfix.mdx +++ b/docs/issues/type/hotfix.mdx @@ -50,10 +50,10 @@ This workflow ensures that critical fixes reach production quickly while maintai name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - deploy-label: deploy // [!code highlight] - hotfix-workflow: hotfix_workflow.yml // [!code highlight] + deploy-label: deploy # [!code highlight] + hotfix-workflow: hotfix_workflow.yml # [!code highlight] ``` @@ -104,7 +104,7 @@ This workflow ensures that critical fixes reach production quickly while maintai // deploy logic here - name: Git Board - Deploy success notification - uses: vypdev/copilot@v2 + uses: vypdev/copilot@v3 if: ${{ success() }} with: single-action: 'deployed_action' @@ -134,9 +134,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - hotfix-label: hotfix // [!code highlight] + hotfix-label: hotfix # [!code highlight] ``` ## Naming @@ -149,9 +149,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - hotfix-tree: hotfix // [!code highlight] + hotfix-tree: hotfix # [!code highlight] ``` Hotfix branches follow this naming convention: @@ -175,9 +175,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - main-branch: master // [!code highlight] + main-branch: master # [!code highlight] ``` ## Images @@ -192,9 +192,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - images-issue-hotfix: url1, url2, url3 // [!code highlight] + images-issue-hotfix: url1, url2, url3 # [!code highlight] ``` diff --git a/docs/issues/type/release.mdx b/docs/issues/type/release.mdx index 3871722d..b48237d0 100644 --- a/docs/issues/type/release.mdx +++ b/docs/issues/type/release.mdx @@ -50,10 +50,10 @@ This workflow ensures that releases are properly planned, tested, and deployed w name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - deploy-label: deploy // [!code highlight] - release-workflow: release_workflow.yml // [!code highlight] + deploy-label: deploy # [!code highlight] + release-workflow: release_workflow.yml # [!code highlight] ``` @@ -104,7 +104,7 @@ This workflow ensures that releases are properly planned, tested, and deployed w // deploy logic here - name: Git Board - Deploy success notification - uses: vypdev/copilot@v2 + uses: vypdev/copilot@v3 if: ${{ success() }} with: single-action: 'deployed_action' @@ -134,9 +134,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - release-label: release // [!code highlight] + release-label: release # [!code highlight] ``` ## Naming @@ -149,9 +149,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - release-tree: release // [!code highlight] + release-tree: release # [!code highlight] ``` Release branches follow this naming convention: @@ -175,9 +175,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - development-branch: development // [!code highlight] + development-branch: development # [!code highlight] ``` ## Images @@ -192,9 +192,9 @@ jobs: name: Git Board - Issue runs-on: ubuntu-latest steps: - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: - images-issue-release: url1, url2, url3 // [!code highlight] + images-issue-release: url1, url2, url3 # [!code highlight] ``` diff --git a/docs/issues/workflow-setup.mdx b/docs/issues/workflow-setup.mdx index b9ce82cb..5c21e801 100644 --- a/docs/issues/workflow-setup.mdx +++ b/docs/issues/workflow-setup.mdx @@ -47,7 +47,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: ${{ vars.PROJECT_IDS }} diff --git a/docs/pull-requests/ai-description.mdx b/docs/pull-requests/ai-description.mdx index d435b2f8..434ebf92 100644 --- a/docs/pull-requests/ai-description.mdx +++ b/docs/pull-requests/ai-description.mdx @@ -22,7 +22,7 @@ No pre-computed file list or patches are sent from the action; the agent has acc The AI is instructed to use your repository's **pull request template** as the structure for the description. You should define: -- **`.github/pull_request_template.md`** — This file is read by the OpenCode agent and used as the **skeleton** to fill. The agent keeps the same headings, bullet lists, checkboxes (`- [ ]`, `- [x]`), and separators, and fills each section with content derived from the diff and the issue. +- **`.github/pull_request_template.md`** — This file is read by the configured `planner` agent runtime and used as the **skeleton** to fill. The agent keeps the same headings, bullet lists, checkboxes (`- [ ]`, `- [x]`), and separators, and fills each section with content derived from the diff and the issue. If you don't have a template, the agent will still produce a structured description, but defining a template ensures consistent, professional PR descriptions that match your team's expectations (e.g. Summary, Related Issues, Scope of Changes, Technical Details, How to Test, Breaking Changes, Deployment Notes, etc.). @@ -43,7 +43,7 @@ If you don't have a template, the agent will still produce a structured descript Set `ai-pull-request-description: true` and configure the selected agent in your workflow: ```yaml -- uses: vypdev/copilot@master +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: '2,3' diff --git a/docs/pull-requests/configuration.mdx b/docs/pull-requests/configuration.mdx index 0bbdd5af..c5d98e37 100644 --- a/docs/pull-requests/configuration.mdx +++ b/docs/pull-requests/configuration.mdx @@ -20,7 +20,7 @@ These inputs apply when the action runs on `pull_request` events. For the comple | Input | Description | Default | |-------|-------------|---------| -| `ai-pull-request-description` | Enable AI-generated PR descriptions (requires OpenCode) | "true" | +| `ai-pull-request-description` | Enable AI-generated PR descriptions using the configured `planner` agent runtime | "true" | | `pull-requests-locale` | Target locale for PR review comment translation | "en-US" | | `ai-members-only` | Restrict AI PR description to org/project members only | "false" | diff --git a/docs/pull-requests/examples.mdx b/docs/pull-requests/examples.mdx index f4e92181..f04d1296 100644 --- a/docs/pull-requests/examples.mdx +++ b/docs/pull-requests/examples.mdx @@ -26,7 +26,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: ${{ vars.PROJECT_IDS }} @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} ai-pull-request-description: true @@ -73,14 +73,14 @@ jobs: agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} ``` -The PR must have an **issue linked** (via branch name) and the issue must have a **non-empty description**. See [AI PR description](/pull-requests/ai-description). +A linked issue with a non-empty description enriches the generated result, but it is **optional**. PRs without a linked issue are supported; the branch diff and repository template still provide the description context. See [AI PR description](/pull-requests/ai-description). ## Example: Project columns and reviewers Link PRs to a board and assign two reviewers: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: "2,3" @@ -95,7 +95,7 @@ Ensure the project has a column named **"In Review"** (or use your actual column Enable images in PR comments and set URLs for feature PRs: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} images-on-pull-request: true diff --git a/docs/pull-requests/workflow-setup.mdx b/docs/pull-requests/workflow-setup.mdx index ed1dbb36..1703fd67 100644 --- a/docs/pull-requests/workflow-setup.mdx +++ b/docs/pull-requests/workflow-setup.mdx @@ -48,7 +48,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} project-ids: ${{ vars.PROJECT_IDS }} diff --git a/docs/quick-start.mdx b/docs/quick-start.mdx index 1fae871b..b7039b83 100644 --- a/docs/quick-start.mdx +++ b/docs/quick-start.mdx @@ -22,7 +22,7 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v4 - - uses: vypdev/copilot@master + - uses: vypdev/copilot@v3 with: token: ${{ secrets.COPILOT_TOKEN }} agent-provider: codex @@ -33,7 +33,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ``` -Pin a released Action ref in production. The example uses `master` only to show the current contract. +The example uses the stable `v3` major ref. For strict reproducibility, replace it with an immutable release tag after selecting the version you have validated. ## Verify before enabling writes diff --git a/docs/security-operations/operations/cli-provisioning.mdx b/docs/security-operations/operations/cli-provisioning.mdx index 4db6fe6f..8c5d0a94 100644 --- a/docs/security-operations/operations/cli-provisioning.mdx +++ b/docs/security-operations/operations/cli-provisioning.mdx @@ -1,6 +1,6 @@ --- title: Agent CLI provisioning -description: Provision pinned OpenCode, Codex, and Cursor CLI runtimes for Bugbot. +description: Provision pinned OpenCode, Codex, and Cursor CLI runtimes for Copilot AI features. --- # Agent CLI provisioning diff --git a/docs/security-operations/operations/provisioning.mdx b/docs/security-operations/operations/provisioning.mdx index 82a7fa25..4ae2759e 100644 --- a/docs/security-operations/operations/provisioning.mdx +++ b/docs/security-operations/operations/provisioning.mdx @@ -4,12 +4,12 @@ description: Pinned installation and verification of the selected agent CLI. --- # CLI provisioning -When running in GitHub Actions, the Action provisions and verifies the selected runtime. It first reuses an executable already present on the runner (`AGENT_PROVISIONING=auto`, the default), which allows a preinitialized Codex CLI to run without a credential or package version in the repository. If the executable is absent, versions MUST be explicit. It MUST use `pnpm`, never `npm`, `npx`, Yarn, Bun, or an unpinned installer. A setup workflow using `ubuntu-latest` therefore needs an approved version variable when the selected CLI is not already installed on the runner. +When running in GitHub Actions, the Action provisions and verifies the selected runtime. It first reuses an executable already present on the runner (`AGENT_PROVISIONING=auto`, the default), which allows a preinitialized Codex CLI to run without a credential or package version in the repository. If the executable is absent, versions MUST be explicit. Codex and OpenCode are installed with the runner's system `npm`; Cursor uses its official installer with checksum verification. The Action never uses `npx`, Yarn, Bun, or an unpinned installer. A setup workflow using `ubuntu-latest` therefore needs an approved version variable when the selected CLI is not already installed on the runner. | Runtime | Provisioning source | Required pin | | --- | --- | --- | -| OpenCode | `opencode-ai@` through Corepack pnpm | `OPENCODE_VERSION` only when absent | -| Codex | `@openai/codex@` through Corepack pnpm | `CODEX_VERSION` only when absent | +| OpenCode | `opencode-ai@` through runner `npm` | `OPENCODE_VERSION` only when absent | +| Codex | `@openai/codex@` through runner `npm` | `CODEX_VERSION` only when absent | | Cursor | Official installer with SHA-256 verification | `CURSOR_INSTALLER_SHA256` only when absent | Provisioning failure is terminal. The Action MUST NOT install another runtime as a fallback. diff --git a/docs/security-operations/operations/troubleshooting.mdx b/docs/security-operations/operations/troubleshooting.mdx index 5d140ed9..2f0061a6 100644 --- a/docs/security-operations/operations/troubleshooting.mdx +++ b/docs/security-operations/operations/troubleshooting.mdx @@ -71,7 +71,7 @@ This guide helps you resolve common issues you might encounter while using Copil - - **Bot didn't run autofix**: OpenCode must be configured and the comment must be interpreted as a fix request (e.g. "fix it", "arregla", "fix all"). There must be at least one unresolved finding. On issue comments, the action needs an open PR that references the issue so it can resolve the branch to checkout and push; otherwise autofix is skipped. + - **Bot didn't run autofix**: A configured `fixer` agent runtime must be available and the comment must be interpreted as a fix request (e.g. "fix it", "arregla", "fix all"). There must be at least one unresolved finding. On issue comments, the action needs an open PR that references the issue so it can resolve the branch to checkout and push; otherwise autofix is skipped. - **Commit not made**: Verify commands (`bugbot-fix-verify-commands`) run after the configured agent applies changes; if any command fails, the action does not commit. If there are no file changes after the fix, nothing is committed. If push fails (e.g. conflict or permissions), check workflow `contents: write` and that the token can push to the branch. diff --git a/docs/security-operations/operations/upgrade-rollback.mdx b/docs/security-operations/operations/upgrade-rollback.mdx index 5df77317..546dd29b 100644 --- a/docs/security-operations/operations/upgrade-rollback.mdx +++ b/docs/security-operations/operations/upgrade-rollback.mdx @@ -22,7 +22,7 @@ copilot --version For a controlled rollout or rollback, install a specific package version explicitly: ```bash -VERSION=3.2.0 # replace with the selected release version +VERSION= npm install --global "@vypdev/copilot@${VERSION}" copilot --version ``` diff --git a/docs/security-operations/operations/version-pinning.mdx b/docs/security-operations/operations/version-pinning.mdx index a31cd748..21d5e7a9 100644 --- a/docs/security-operations/operations/version-pinning.mdx +++ b/docs/security-operations/operations/version-pinning.mdx @@ -9,14 +9,14 @@ Every runtime must have a deliberate version policy. A missing version is not an | Runtime | Version input | Installation boundary | Verification | | --- | --- | --- | --- | -| Codex | `CODEX_VERSION` | Corepack pnpm global install | CLI version output | -| OpenCode | `OPENCODE_VERSION` | Corepack pnpm global install | CLI version output | +| Codex | `CODEX_VERSION` | Runner `npm` global install | CLI version output | +| OpenCode | `OPENCODE_VERSION` | Runner `npm` global install | CLI version output | | Cursor | installer and `CURSOR_INSTALLER_SHA256` | Official installer download | SHA-256 comparison | The Copilot CLI is distributed as `@vypdev/copilot` and exposes the `copilot` executable. Pin it for reproducible local tooling: ```bash -VERSION=3.2.0 # replace with the selected release version +VERSION= npm install --global "@vypdev/copilot@${VERSION}" copilot --version ``` diff --git a/docs/security-operations/security/credentials.mdx b/docs/security-operations/security/credentials.mdx index 11d98a9c..66aa7565 100644 --- a/docs/security-operations/security/credentials.mdx +++ b/docs/security-operations/security/credentials.mdx @@ -6,11 +6,15 @@ description: Credential references, storage rules, and authentication boundaries Credentials are references, not configuration values. They MUST be injected by the runner environment and MUST NOT appear in action inputs, command arguments, prompts, repository files, comments, or logs. +An expression that references a protected Secret, such as `${{ secrets.CURSOR_API_KEY }}`, +is safe to keep in workflow YAML; the prohibition applies to the literal credential +value, not to the Secret reference. + | Runtime | Accepted reference | Prohibited export | | --- | --- | --- | | OpenCode | Provider-specific environment credential or controlled local login | OAuth/session files in CI | | Codex | `CODEX_ACCESS_TOKEN`, `OPENAI_API_KEY`, or local session on a controlled self-hosted runner | Personal session tokens in hosted CI | -| Cursor | `CURSOR_API_KEY` | API key in workflow YAML | +| Cursor | `CURSOR_API_KEY` | Literal API key value in workflow YAML; `${{ secrets.CURSOR_API_KEY }}` is the supported reference | Authentication preflight MUST fail closed when the selected runtime has no usable credential reference. A binary existing on `PATH` is not authentication evidence. diff --git a/docs/single-actions/available-actions.mdx b/docs/single-actions/available-actions.mdx index fc8cba12..b95d7437 100644 --- a/docs/single-actions/available-actions.mdx +++ b/docs/single-actions/available-actions.mdx @@ -26,7 +26,7 @@ These actions need **`single-action-issue`** set to the issue number. The workfl | **`initial_setup`** | — | Performs **initial setup**: creates labels, issue types (if supported), verifies access. If the repo has no version tags, creates default tag **`v1.0.0`** so release/hotfix issues get a base version. No issue required. | First-time repo setup; run once or when you add new labels/types. Run on new repos before creating release/hotfix issues to avoid "Unknown Version" loops. | | **`create_release`** | `single-action-version`, `single-action-title`, `single-action-changelog` | Creates a **GitHub release** with the given version, title, and changelog (markdown body). | From a workflow after tests pass; use version and changelog from your build or inputs. | | **`create_tag`** | `single-action-version` | Creates a **Git tag** with prefix `v` (e.g. `v1.2.3`) for the given version from the release branch. | When you only need a tag (e.g. for versioning) without a full release. The tag is created from the `releaseBranch` stored in issue configuration. | -| **`publish_github_action`** | `single-action-version` | **Publishes or updates** the GitHub Action: creates/updates the major version tag (e.g. `v2` from `v2.0.4`) and the corresponding GitHub Release. Requires that `create_tag` has been run first to create the source tag `v{version}`. | In a CI job that builds and publishes the action, after `create_tag` and `create_release` have run. | +| **`publish_github_action`** | `single-action-version` | **Publishes or updates** the GitHub Action: creates/updates the major version tag (for example, `v3` from a `v3.x.y` release) and the corresponding GitHub Release. Requires that `create_tag` has been run first to create the source tag `v{version}`. | In a CI job that builds and publishes the action, after `create_tag` and `create_release` have run. | ## Actions that fail the job on failure diff --git a/docs/single-actions/configuration.mdx b/docs/single-actions/configuration.mdx index 628c9918..de69383d 100644 --- a/docs/single-actions/configuration.mdx +++ b/docs/single-actions/configuration.mdx @@ -40,7 +40,7 @@ For **`create_release`** only: ## Example: workflow with single action ```yaml -- uses: vypdev/copilot@master +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: check_progress_action @@ -50,7 +50,7 @@ For **`create_release`** only: For release: ```yaml -- uses: vypdev/copilot@master +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: create_release diff --git a/docs/single-actions/examples.mdx b/docs/single-actions/examples.mdx index 9d868252..d57b6ec5 100644 --- a/docs/single-actions/examples.mdx +++ b/docs/single-actions/examples.mdx @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 with: ref: feature/123-add-login # optional: branch to analyze - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: check_progress_action @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: detect_potential_problems_action @@ -56,7 +56,7 @@ See [Bugbot](/bugbot) for full documentation. Get implementation steps for issue `789` and post them as a comment: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: recommend_steps_action @@ -84,7 +84,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: vypdev/copilot@v2 + - uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: think_action @@ -98,7 +98,7 @@ For a **question** from the CLI, use the **CLI** (see below); the workflow `thin Create a GitHub release with version, title, and changelog: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: create_release @@ -118,7 +118,7 @@ Changelog can be read from a file or generated in a previous step and passed as Create a Git tag with prefix `v` (e.g. `v1.2.0`) from the release branch: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: create_tag @@ -133,7 +133,7 @@ Create a Git tag with prefix `v` (e.g. `v1.2.0`) from the release branch: Mark issue `100` as deployed (e.g. from your release workflow): ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: deployed_action @@ -145,7 +145,7 @@ Mark issue `100` as deployed (e.g. from your release workflow): Run initial setup (labels, issue types, verify access). If the repo has no version tags, Copilot creates default tag **`v1.0.0`** so release/hotfix issues get a base version and do not get stuck in an "Unknown Version" loop: ```yaml -- uses: vypdev/copilot@v2 +- uses: vypdev/copilot@v3 with: token: ${{ secrets.PAT }} single-action: initial_setup @@ -157,7 +157,7 @@ Often run once per repo or after adding new label/type config. On new repos, run ## CLI examples -Run the CLI from the **target repository root**. `copilot setup` and `copilot doctor` prompt for the setup PAT; other commands accept `--token` or `PERSONAL_ACCESS_TOKEN` from the environment. The first five commands mirror single actions; `copilot do` is CLI-only. See [Workflow & CLI](/single-actions/workflow-and-cli) for installation, updates, and the complete option reference. +Run repository-dependent commands from the **target repository root**. `copilot setup` and `copilot doctor` prompt for the setup PAT; other GitHub commands accept `--token` or `PERSONAL_ACCESS_TOKEN` from the environment. `copilot upgrade`, `copilot --version`, and help can run from any directory. `copilot check-progress`, `copilot detect-potential-problems`, `copilot recommend-steps`, and `copilot think` correspond to supported action capabilities; `copilot setup`, `copilot doctor`, and `copilot do` are CLI-specific workflows. See [Workflow & CLI](/single-actions/workflow-and-cli) for installation, updates, and the complete option reference. ### setup diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index 171eb6b5..8d62cd96 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -28,7 +28,7 @@ Use `workflow_dispatch` to run on demand, or trigger the workflow from another e ## Install or update the CLI -The published package is `@vypdev/copilot` and exposes one global executable: **`copilot`**. Install it from npm, then run commands from the target repository. +The published package is `@vypdev/copilot` and exposes one global executable: **`copilot`**. Install it from npm, then run repository-dependent commands from the target repository. ### Install the published package @@ -68,6 +68,8 @@ copilot --version Run `copilot upgrade` from any directory. It updates the published npm installation and does not require a target repository or GitHub token. If the installed version does not include this command, run `npm install --global @vypdev/copilot@latest` once and then use `copilot upgrade`. +Normal Copilot CLI commands also perform a lightweight update check against npm. It is informational only: the result is cached locally for 24 hours, the network request has a short timeout, and failures are ignored so the command continues normally. When a newer release is found, the CLI prints `A new version (x.y.z) is available. Run "copilot upgrade".` The check is skipped for `copilot upgrade`, `--version`, and help. Set `COPILOT_DISABLE_UPDATE_CHECK=1` to opt out. + Verify the installed package and executable: ```bash @@ -101,7 +103,7 @@ For an npm installation, `npm prefix --global` shows the npm global prefix; on m ## Local CLI prerequisites -Run every command from the **root of the target repository**, not necessarily from the Copilot checkout. The target repository must: +Run repository-dependent commands from the **root of the target repository**, not necessarily from the Copilot checkout. `copilot upgrade`, `copilot --version`, and help can run from any directory. The target repository must: - be a Git worktree; - have an `origin` remote pointing to a GitHub repository; diff --git a/package.json b/package.json index 42f81606..25f366dd 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "lint": "eslint src", "validate:agent-docs": "node scripts/validate-agent-documentation.cjs", "validate:docs-page": "node scripts/validate-docs-page-assets.cjs", + "validate:documentation": "node scripts/validate-documentation-contract.cjs", "validate:workflows": "node scripts/validate-workflow-contract.cjs", "lint:fix": "eslint src --fix", "postinstall": "node scripts/install-git-hooks.cjs" diff --git a/scripts/validate-documentation-contract.cjs b/scripts/validate-documentation-contract.cjs new file mode 100644 index 00000000..6c1cc52a --- /dev/null +++ b/scripts/validate-documentation-contract.cjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); + +const root = path.resolve(__dirname, '..'); +const docsRoot = path.join(root, 'docs'); +const navigation = JSON.parse(fs.readFileSync(path.join(root, 'docs.json'), 'utf8')); +const action = yaml.load(fs.readFileSync(path.join(root, 'action.yml'), 'utf8')); +const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + +const errors = []; +const docsFiles = fs.readdirSync(docsRoot, { recursive: true }) + .filter(file => file.endsWith('.mdx')) + .map(file => String(file)); +const docsContent = docsFiles.map(file => fs.readFileSync(path.join(docsRoot, file), 'utf8')); +const allDocumentation = [ + fs.readFileSync(path.join(root, 'README.md'), 'utf8'), + ...docsContent, +].join('\n'); + +const routes = new Set(); +function collectRoutes(value) { + if (Array.isArray(value)) { + value.forEach(collectRoutes); + return; + } + if (!value || typeof value !== 'object') return; + if (typeof value.href === 'string' && value.href.startsWith('/')) routes.add(value.href); + Object.values(value).forEach(collectRoutes); +} +collectRoutes(navigation); + +function routeForFile(file) { + const withoutExtension = file.slice(0, -'.mdx'.length); + if (withoutExtension === 'index') return '/'; + if (withoutExtension.endsWith('/index')) return `/${withoutExtension.slice(0, -'/index'.length)}`; + return `/${withoutExtension}`; +} + +for (const file of docsFiles) { + const route = routeForFile(file); + if (!routes.has(route)) errors.push(`${file}: public MDX page is not registered in docs.json as ${route}`); +} + +function assertRoute(route, source) { + const normalized = route.split('#', 1)[0] || '/'; + if (!routes.has(normalized)) errors.push(`${source}: local documentation link targets an unregistered route ${normalized}`); +} + +for (const [index, source] of docsContent.entries()) { + const file = docsFiles[index]; + for (const match of source.matchAll(/\]\((\/[^)\s]+)(?:\s+[^)]*)?\)/g)) assertRoute(match[1], `${file}: markdown link`); + for (const match of source.matchAll(/\bhref=["'](\/[^"']+)/g)) assertRoute(match[1], `${file}: href`); + + for (const match of source.matchAll(/^```(?:yaml|yml)\s*\n([\s\S]*?)^```\s*$/gm)) { + try { + yaml.load(match[1]); + } catch (error) { + const line = source.slice(0, match.index).split('\n').length; + errors.push(`${file}:${line}: invalid YAML documentation snippet: ${error.message}`); + } + } +} + +const expectedActionMajor = `v${String(packageJson.version).split('.')[0]}`; +for (const match of allDocumentation.matchAll(/uses:\s*vypdev\/copilot@([^\s"'`]+)/g)) { + if (match[1] !== expectedActionMajor) { + errors.push(`documentation uses vypdev/copilot@${match[1]}; expected the published major ref ${expectedActionMajor}`); + } +} + +const tick = String.fromCharCode(96); +const undocumentedInputs = Object.keys(action.inputs ?? {}) + .filter(input => !allDocumentation.includes(`${tick}${input}${tick}`)); +if (undocumentedInputs.length) { + errors.push(`action.yml inputs missing from documentation: ${undocumentedInputs.join(', ')}`); +} + +if (/\bgiik\b/i.test(allDocumentation)) errors.push('documentation contains the obsolete product name giik'); + +if (errors.length) { + console.error(errors.join('\n')); + process.exit(1); +} + +console.log(`documentation contract validation: PASS (${docsFiles.length} MDX pages, ${routes.size} registered routes, ${Object.keys(action.inputs ?? {}).length} documented action inputs)`); diff --git a/src/application/ports/cli_update_check_ports.ts b/src/application/ports/cli_update_check_ports.ts new file mode 100644 index 00000000..908085f5 --- /dev/null +++ b/src/application/ports/cli_update_check_ports.ts @@ -0,0 +1,4 @@ +/** Retrieves the latest published version of the Copilot CLI. */ +export interface CliUpdateCheckPort { + getLatestPublishedVersion(): Promise; +} diff --git a/src/application/usecases/__tests__/check_cli_update_use_case.test.ts b/src/application/usecases/__tests__/check_cli_update_use_case.test.ts new file mode 100644 index 00000000..5bd465e6 --- /dev/null +++ b/src/application/usecases/__tests__/check_cli_update_use_case.test.ts @@ -0,0 +1,24 @@ +import { CheckCliUpdateUseCase } from '../check_cli_update_use_case'; + +describe('CheckCliUpdateUseCase', () => { + it('returns an update when the published version is newer', async () => { + const getLatestPublishedVersion = jest.fn().mockResolvedValue('3.4.0'); + + await expect(new CheckCliUpdateUseCase({ getLatestPublishedVersion }).execute('3.3.0')).resolves.toEqual({ + installedVersion: '3.3.0', + publishedVersion: '3.4.0', + }); + }); + + it('does not return an update for an equal or older version', async () => { + const getLatestPublishedVersion = jest.fn().mockResolvedValue('3.3.0'); + + await expect(new CheckCliUpdateUseCase({ getLatestPublishedVersion }).execute('3.3.0')).resolves.toBeUndefined(); + }); + + it('ignores an unavailable published version', async () => { + const getLatestPublishedVersion = jest.fn().mockResolvedValue(undefined); + + await expect(new CheckCliUpdateUseCase({ getLatestPublishedVersion }).execute('3.3.0')).resolves.toBeUndefined(); + }); +}); diff --git a/src/application/usecases/check_cli_update_use_case.ts b/src/application/usecases/check_cli_update_use_case.ts new file mode 100644 index 00000000..f22528ff --- /dev/null +++ b/src/application/usecases/check_cli_update_use_case.ts @@ -0,0 +1,19 @@ +import { isNewerCliVersion } from '../../domain/cli_version'; +import type { CliUpdateCheckPort } from '../ports/cli_update_check_ports'; + +export interface CliUpdateAvailable { + installedVersion: string; + publishedVersion: string; +} + +/** Checks for a newer published CLI version without coupling the application to npm. */ +export class CheckCliUpdateUseCase { + constructor(private readonly cliUpdateCheckPort: CliUpdateCheckPort) {} + + async execute(installedVersion: string): Promise { + const publishedVersion = await this.cliUpdateCheckPort.getLatestPublishedVersion(); + if (!publishedVersion || !isNewerCliVersion(installedVersion, publishedVersion)) return undefined; + + return { installedVersion, publishedVersion }; + } +} diff --git a/src/cli/__tests__/cli_program_update_check.test.ts b/src/cli/__tests__/cli_program_update_check.test.ts new file mode 100644 index 00000000..8cf0f6d1 --- /dev/null +++ b/src/cli/__tests__/cli_program_update_check.test.ts @@ -0,0 +1,24 @@ +import { Command } from 'commander'; + +jest.mock('../command_registry', () => ({ + registerCliCommands: (program: Command) => { + program.command('work').action(() => undefined); + return program; + }, +})); + +import { createCliProgram } from '../cli_program'; + +describe('CLI program update check hook', () => { + it('checks for updates before a command and does not alter its execution', async () => { + const execute = jest.fn().mockResolvedValue({ installedVersion: '3.3.0', publishedVersion: '3.4.0' }); + const log = jest.spyOn(console, 'log').mockImplementation(() => {}); + const program = createCliProgram({ execute }); + + await program.parseAsync(['node', 'copilot', 'work']); + + expect(execute).toHaveBeenCalledWith('3.3.0'); + expect(log).toHaveBeenCalledWith('A new version (3.4.0) is available. Run "copilot upgrade".'); + log.mockRestore(); + }); +}); diff --git a/src/cli/__tests__/cli_update_check_policy.test.ts b/src/cli/__tests__/cli_update_check_policy.test.ts new file mode 100644 index 00000000..31a7f38d --- /dev/null +++ b/src/cli/__tests__/cli_update_check_policy.test.ts @@ -0,0 +1,20 @@ +import { + isUpdateCheckDisabled, + shouldCheckForUpdates, + UPDATE_CHECK_DISABLED_ENV, +} from '../cli_update_check_policy'; + +describe('CLI update check policy', () => { + it('skips commands that would be noisy or recursive', () => { + expect(shouldCheckForUpdates('upgrade')).toBe(false); + expect(shouldCheckForUpdates('help')).toBe(false); + expect(shouldCheckForUpdates('setup')).toBe(true); + }); + + it('supports explicit opt-out values', () => { + expect(isUpdateCheckDisabled({ [UPDATE_CHECK_DISABLED_ENV]: 'true' })).toBe(true); + expect(isUpdateCheckDisabled({ [UPDATE_CHECK_DISABLED_ENV]: '1' })).toBe(true); + expect(isUpdateCheckDisabled({ [UPDATE_CHECK_DISABLED_ENV]: 'false' })).toBe(false); + expect(isUpdateCheckDisabled({})).toBe(false); + }); +}); diff --git a/src/cli/__tests__/cli_update_notification.test.ts b/src/cli/__tests__/cli_update_notification.test.ts new file mode 100644 index 00000000..289a4db4 --- /dev/null +++ b/src/cli/__tests__/cli_update_notification.test.ts @@ -0,0 +1,24 @@ +import { notifyAboutCliUpdate } from '../cli_update_notification'; + +describe('CLI update notification', () => { + it('prints a concise advisory when an update is available', async () => { + const log = jest.fn(); + + await notifyAboutCliUpdate( + { execute: jest.fn().mockResolvedValue({ installedVersion: '3.3.0', publishedVersion: '3.4.0' }) }, + '3.3.0', + { log }, + ); + + expect(log).toHaveBeenCalledWith('A new version (3.4.0) is available. Run "copilot upgrade".'); + }); + + it('stays silent when no update exists or the check fails', async () => { + const log = jest.fn(); + + await notifyAboutCliUpdate({ execute: jest.fn().mockResolvedValue(undefined) }, '3.3.0', { log }); + await notifyAboutCliUpdate({ execute: jest.fn().mockRejectedValue(new Error('offline')) }, '3.3.0', { log }); + + expect(log).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/cli_program.ts b/src/cli/cli_program.ts index ba770e63..476994f7 100644 --- a/src/cli/cli_program.ts +++ b/src/cli/cli_program.ts @@ -1,7 +1,10 @@ import { readFileSync } from 'node:fs'; import * as path from 'node:path'; import { Command } from 'commander'; +import { createCliUpdateCheckUseCase } from '../infrastructure/composition/cli_update_check_composition_root'; import { registerCliCommands } from './command_registry'; +import { isUpdateCheckDisabled, shouldCheckForUpdates } from './cli_update_check_policy'; +import { notifyAboutCliUpdate, type CliUpdateChecker } from './cli_update_notification'; function loadPackageVersion(): string { const packagePath = path.join(__dirname, '..', '..', 'package.json'); @@ -9,10 +12,17 @@ function loadPackageVersion(): string { return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0'; } -export function createCliProgram(): Command { +export function createCliProgram( + updateChecker: CliUpdateChecker = createCliUpdateCheckUseCase(), +): Command { + const installedVersion = loadPackageVersion(); const program = new Command() .name('copilot') .description('GitHub workflow automation and repository management CLI') - .version(loadPackageVersion(), '-V, --version', 'Display the installed Copilot version'); + .version(installedVersion, '-V, --version', 'Display the installed Copilot version'); + program.hook('preAction', async (_thisCommand, actionCommand) => { + if (isUpdateCheckDisabled() || !shouldCheckForUpdates(actionCommand.name())) return; + await notifyAboutCliUpdate(updateChecker, installedVersion); + }); return registerCliCommands(program); } diff --git a/src/cli/cli_update_check_policy.ts b/src/cli/cli_update_check_policy.ts new file mode 100644 index 00000000..3b794a73 --- /dev/null +++ b/src/cli/cli_update_check_policy.ts @@ -0,0 +1,13 @@ +const UPDATE_CHECK_DISABLED_VALUES = new Set(['1', 'true', 'yes', 'on']); +const COMMANDS_WITHOUT_UPDATE_CHECK = new Set(['help', 'upgrade']); + +export const UPDATE_CHECK_DISABLED_ENV = 'COPILOT_DISABLE_UPDATE_CHECK'; + +export function isUpdateCheckDisabled(environment: NodeJS.ProcessEnv = process.env): boolean { + const value = environment[UPDATE_CHECK_DISABLED_ENV]?.trim().toLowerCase(); + return value !== undefined && UPDATE_CHECK_DISABLED_VALUES.has(value); +} + +export function shouldCheckForUpdates(commandName: string): boolean { + return !COMMANDS_WITHOUT_UPDATE_CHECK.has(commandName); +} diff --git a/src/cli/cli_update_notification.ts b/src/cli/cli_update_notification.ts new file mode 100644 index 00000000..6a2f854e --- /dev/null +++ b/src/cli/cli_update_notification.ts @@ -0,0 +1,21 @@ +import type { CliUpdateAvailable } from '../application/usecases/check_cli_update_use_case'; + +export interface CliUpdateChecker { + execute(installedVersion: string): Promise; +} + +/** Displays advisory update information while keeping update failures invisible to users. */ +export async function notifyAboutCliUpdate( + checker: CliUpdateChecker, + installedVersion: string, + output: Pick = console, +): Promise { + try { + const update = await checker.execute(installedVersion); + if (update) { + output.log(`A new version (${update.publishedVersion}) is available. Run "copilot upgrade".`); + } + } catch { + // Version checks are advisory and must never change the command outcome. + } +} diff --git a/src/domain/__tests__/cli_version.test.ts b/src/domain/__tests__/cli_version.test.ts new file mode 100644 index 00000000..ebf8b7f8 --- /dev/null +++ b/src/domain/__tests__/cli_version.test.ts @@ -0,0 +1,30 @@ +import { compareCliVersions, isNewerCliVersion } from '../cli_version'; + +describe('CLI version policy', () => { + it('recognizes newer release versions', () => { + expect(compareCliVersions('3.3.0', '3.4.0')).toBe(-1); + expect(isNewerCliVersion('3.3.0', '3.4.0')).toBe(true); + }); + + it('does not report equal or older published versions', () => { + expect(isNewerCliVersion('3.3.0', '3.3.0')).toBe(false); + expect(isNewerCliVersion('3.4.0', '3.3.0')).toBe(false); + }); + + it('applies semver prerelease precedence', () => { + expect(compareCliVersions('3.3.0-beta.2', '3.3.0-beta.10')).toBe(-1); + expect(compareCliVersions('3.3.0-beta.10', '3.3.0')).toBe(-1); + expect(compareCliVersions('v3.3.0', '3.3.0')).toBe(0); + expect(compareCliVersions('3.2.0', '3.3.0')).toBe(-1); + expect(compareCliVersions('4.0.0', '3.3.9')).toBe(1); + expect(compareCliVersions('3.3.1', '3.3.0')).toBe(1); + expect(compareCliVersions('3.3.0-1', '3.3.0-alpha')).toBe(-1); + expect(compareCliVersions('3.3.0-alpha', '3.3.0-1')).toBe(1); + expect(compareCliVersions('3.3.0-alpha', '3.3.0-alpha.1')).toBe(-1); + }); + + it('returns undefined for malformed versions', () => { + expect(compareCliVersions('3.3', '3.4.0')).toBeUndefined(); + expect(compareCliVersions('3.3.0', 'latest')).toBeUndefined(); + }); +}); diff --git a/src/domain/cli_version.ts b/src/domain/cli_version.ts new file mode 100644 index 00000000..3b1a6771 --- /dev/null +++ b/src/domain/cli_version.ts @@ -0,0 +1,63 @@ +interface ParsedCliVersion { + major: number; + minor: number; + patch: number; + prerelease: string[]; +} + +const CLI_VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; + +function parseCliVersion(version: string): ParsedCliVersion | undefined { + const match = CLI_VERSION_PATTERN.exec(version.trim()); + if (!match) return undefined; + + return { + major: Number.parseInt(match[1], 10), + minor: Number.parseInt(match[2], 10), + patch: Number.parseInt(match[3], 10), + prerelease: match[4]?.split('.') ?? [], + }; +} + +function comparePrereleaseIdentifiers(left: string, right: string): number { + const leftNumber = /^\d+$/.test(left) ? Number.parseInt(left, 10) : undefined; + const rightNumber = /^\d+$/.test(right) ? Number.parseInt(right, 10) : undefined; + + if (leftNumber !== undefined && rightNumber !== undefined) return Math.sign(leftNumber - rightNumber); + if (leftNumber !== undefined) return -1; + if (rightNumber !== undefined) return 1; + return left < right ? -1 : left > right ? 1 : 0; +} + +/** Compares two CLI versions using release and prerelease precedence. */ +export function compareCliVersions(left: string, right: string): number | undefined { + const leftVersion = parseCliVersion(left); + const rightVersion = parseCliVersion(right); + if (!leftVersion || !rightVersion) return undefined; + + for (const component of ['major', 'minor', 'patch'] as const) { + if (leftVersion[component] !== rightVersion[component]) { + return leftVersion[component] < rightVersion[component] ? -1 : 1; + } + } + + if (leftVersion.prerelease.length === 0 && rightVersion.prerelease.length > 0) return 1; + if (leftVersion.prerelease.length > 0 && rightVersion.prerelease.length === 0) return -1; + + const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length); + for (let index = 0; index < length; index += 1) { + const leftIdentifier = leftVersion.prerelease[index]; + const rightIdentifier = rightVersion.prerelease[index]; + if (leftIdentifier === undefined) return -1; + if (rightIdentifier === undefined) return 1; + const comparison = comparePrereleaseIdentifiers(leftIdentifier, rightIdentifier); + if (comparison !== 0) return comparison; + } + + return 0; +} + +/** Returns true only when the published version is newer than the installed one. */ +export function isNewerCliVersion(installedVersion: string, publishedVersion: string): boolean { + return compareCliVersions(installedVersion, publishedVersion) === -1; +} diff --git a/src/infrastructure/cli/__tests__/npm_cli_update_check_adapter.test.ts b/src/infrastructure/cli/__tests__/npm_cli_update_check_adapter.test.ts new file mode 100644 index 00000000..cd238491 --- /dev/null +++ b/src/infrastructure/cli/__tests__/npm_cli_update_check_adapter.test.ts @@ -0,0 +1,115 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + FileCliUpdateCheckCache, + NPM_REGISTRY_URL, + NpmCliUpdateCheckAdapter, + resolveUpdateCheckCachePath, +} from '../npm_cli_update_check_adapter'; + +class MemoryCache { + entry: { checkedAt: number; latestVersion?: string } | undefined; + + read() { + return this.entry; + } + + write(entry: { checkedAt: number; latestVersion?: string }) { + this.entry = entry; + } +} + +describe('NpmCliUpdateCheckAdapter', () => { + it('reads the latest dist-tag from npm and caches it', async () => { + const cache = new MemoryCache(); + const fetcher = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ 'dist-tags': { latest: '3.4.0' } }), + }); + + await expect(new NpmCliUpdateCheckAdapter({ + cache, + fetcher, + now: () => 1000, + }).getLatestPublishedVersion()).resolves.toBe('3.4.0'); + + expect(fetcher).toHaveBeenCalledWith(NPM_REGISTRY_URL, expect.objectContaining({ + headers: { accept: 'application/json' }, + signal: expect.any(AbortSignal), + })); + expect(cache.entry).toEqual({ checkedAt: 1000, latestVersion: '3.4.0' }); + }); + + it('uses a fresh cache without contacting npm', async () => { + const cache = new MemoryCache(); + cache.entry = { checkedAt: 1000, latestVersion: '3.4.0' }; + const fetcher = jest.fn(); + + await expect(new NpmCliUpdateCheckAdapter({ cache, fetcher, now: () => 1001 }).getLatestPublishedVersion()) + .resolves.toBe('3.4.0'); + expect(fetcher).not.toHaveBeenCalled(); + }); + + it('fails silently and caches the failed check when npm is unavailable', async () => { + const cache = new MemoryCache(); + const fetcher = jest.fn().mockRejectedValue(new Error('offline')); + + await expect(new NpmCliUpdateCheckAdapter({ cache, fetcher, now: () => 2000 }).getLatestPublishedVersion()) + .resolves.toBeUndefined(); + expect(cache.entry).toEqual({ checkedAt: 2000 }); + }); + + it('does not fail when the cache store itself is unavailable', async () => { + const fetcher = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ 'dist-tags': { latest: '3.4.0' } }), + }); + const cache = { + read: () => { throw new Error('cache read failed'); }, + write: () => { throw new Error('cache write failed'); }, + }; + + await expect(new NpmCliUpdateCheckAdapter({ cache, fetcher, now: () => 3000 }).getLatestPublishedVersion()) + .resolves.toBe('3.4.0'); + }); + + it('aborts a slow registry request at the configured timeout', async () => { + const cache = new MemoryCache(); + const fetcher = jest.fn() as jest.MockedFunction; + fetcher.mockImplementation((_url, init) => new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => reject(new Error('aborted')), { once: true }); + })); + + await expect(new NpmCliUpdateCheckAdapter({ cache, fetcher, now: () => 4000, timeoutMs: 1 }) + .getLatestPublishedVersion()).resolves.toBeUndefined(); + expect(cache.entry).toEqual({ checkedAt: 4000 }); + }); + + it('persists valid cache entries and ignores malformed files', () => { + const directory = mkdtempSync(join(tmpdir(), 'copilot-update-cache-')); + const filePath = join(directory, 'nested', 'update-check.json'); + const cache = new FileCliUpdateCheckCache(filePath); + + cache.write({ checkedAt: 5000, latestVersion: '3.4.0' }); + expect(cache.read()).toEqual({ checkedAt: 5000, latestVersion: '3.4.0' }); + + writeFileSync(filePath, 'null'); + expect(cache.read()).toBeUndefined(); + writeFileSync(filePath, '{"checkedAt":"invalid"}'); + expect(cache.read()).toBeUndefined(); + writeFileSync(filePath, '{"checkedAt":5000,"latestVersion":42}'); + expect(cache.read()).toEqual({ checkedAt: 5000 }); + + rmSync(directory, { recursive: true, force: true }); + }); + + it('resolves platform-appropriate cache locations', () => { + expect(resolveUpdateCheckCachePath('darwin', { XDG_CACHE_HOME: '/tmp/cache' }, '/Users/test')) + .toBe('/tmp/cache/copilot/update-check.json'); + expect(resolveUpdateCheckCachePath('win32', { LOCALAPPDATA: 'C:\\Users\\test\\AppData\\Local' }, '/Users/test')) + .toBe('C:\\Users\\test\\AppData\\Local/copilot/update-check.json'); + }); +}); diff --git a/src/infrastructure/cli/npm_cli_update_check_adapter.ts b/src/infrastructure/cli/npm_cli_update_check_adapter.ts new file mode 100644 index 00000000..ed2c7bd5 --- /dev/null +++ b/src/infrastructure/cli/npm_cli_update_check_adapter.ts @@ -0,0 +1,133 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { dirname, join } from 'node:path'; +import type { CliUpdateCheckPort } from '../../application/ports/cli_update_check_ports'; +import { COPILOT_PACKAGE_NAME } from './npm_cli_upgrade_adapter'; + +export const NPM_REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(COPILOT_PACKAGE_NAME)}`; +export const UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1000; +export const UPDATE_CHECK_TIMEOUT_MS = 1500; + +export interface UpdateCheckCacheEntry { + checkedAt: number; + latestVersion?: string; +} + +export interface CliUpdateCheckCache { + read(): UpdateCheckCacheEntry | undefined; + write(entry: UpdateCheckCacheEntry): void; +} + +export function resolveUpdateCheckCachePath( + platform: NodeJS.Platform = process.platform, + environment: NodeJS.ProcessEnv = process.env, + homeDirectory: string = homedir(), +): string { + const cacheRoot = platform === 'win32' + ? environment.LOCALAPPDATA || join(homeDirectory, 'AppData', 'Local') + : environment.XDG_CACHE_HOME || join(homeDirectory, '.cache'); + return join(cacheRoot, 'copilot', 'update-check.json'); +} + +export class FileCliUpdateCheckCache implements CliUpdateCheckCache { + constructor(private readonly filePath: string = resolveUpdateCheckCachePath()) {} + + read(): UpdateCheckCacheEntry | undefined { + try { + const value: unknown = JSON.parse(readFileSync(this.filePath, 'utf8')); + if (!value || typeof value !== 'object') return undefined; + const entry = value as Record; + if (typeof entry.checkedAt !== 'number' || !Number.isFinite(entry.checkedAt)) return undefined; + return { + checkedAt: entry.checkedAt, + ...(typeof entry.latestVersion === 'string' ? { latestVersion: entry.latestVersion } : {}), + }; + } catch { + return undefined; + } + } + + write(entry: UpdateCheckCacheEntry): void { + try { + mkdirSync(dirname(this.filePath), { recursive: true }); + writeFileSync(this.filePath, `${JSON.stringify(entry)}\n`, { encoding: 'utf8', mode: 0o600 }); + } catch { + // A cache failure must not affect the CLI command. + } + } +} + +interface NpmRegistryPackageMetadata { + 'dist-tags'?: { + latest?: unknown; + }; +} + +export interface NpmCliUpdateCheckAdapterOptions { + cache?: CliUpdateCheckCache; + fetcher?: typeof fetch; + now?: () => number; + cacheTtlMs?: number; + timeoutMs?: number; +} + +/** Reads npm's latest dist-tag with bounded latency and a non-sensitive local cache. */ +export class NpmCliUpdateCheckAdapter implements CliUpdateCheckPort { + private readonly cache: CliUpdateCheckCache; + private readonly fetcher: typeof fetch; + private readonly now: () => number; + private readonly cacheTtlMs: number; + private readonly timeoutMs: number; + + constructor(options: NpmCliUpdateCheckAdapterOptions = {}) { + this.cache = options.cache ?? new FileCliUpdateCheckCache(); + this.fetcher = options.fetcher ?? fetch; + this.now = options.now ?? Date.now; + this.cacheTtlMs = options.cacheTtlMs ?? UPDATE_CHECK_CACHE_TTL_MS; + this.timeoutMs = options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS; + } + + async getLatestPublishedVersion(): Promise { + const checkedAt = this.now(); + let cached: UpdateCheckCacheEntry | undefined; + try { + cached = this.cache.read(); + } catch { + cached = undefined; + } + if (cached && checkedAt >= cached.checkedAt && checkedAt - cached.checkedAt < this.cacheTtlMs) { + return cached.latestVersion; + } + + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const response = await this.fetcher(NPM_REGISTRY_URL, { + headers: { accept: 'application/json' }, + signal: controller.signal, + }); + if (!response.ok) throw new Error(`npm registry returned HTTP ${response.status}`); + const payload = await response.json() as NpmRegistryPackageMetadata; + const latestVersion = typeof payload['dist-tags']?.latest === 'string' + ? payload['dist-tags'].latest + : undefined; + this.writeCache({ checkedAt, ...(latestVersion ? { latestVersion } : {}) }); + return latestVersion; + } finally { + clearTimeout(timeout); + } + } catch { + this.writeCache({ checkedAt }); + return undefined; + } + } + + private writeCache(entry: UpdateCheckCacheEntry): void { + try { + this.cache.write(entry); + } catch { + // A cache failure must not affect the CLI command. + } + } +} diff --git a/src/infrastructure/composition/cli_update_check_composition_root.ts b/src/infrastructure/composition/cli_update_check_composition_root.ts new file mode 100644 index 00000000..34731ade --- /dev/null +++ b/src/infrastructure/composition/cli_update_check_composition_root.ts @@ -0,0 +1,6 @@ +import { CheckCliUpdateUseCase } from '../../application/usecases/check_cli_update_use_case'; +import { NpmCliUpdateCheckAdapter } from '../cli/npm_cli_update_check_adapter'; + +export function createCliUpdateCheckUseCase(): CheckCliUpdateUseCase { + return new CheckCliUpdateUseCase(new NpmCliUpdateCheckAdapter()); +} From fc0ba0bceaaa8eb61068e7e028af1547db5b9ad0 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Thu, 3 Sep 2026 04:27:24 +0200 Subject: [PATCH 04/11] develop: add configurable PR policies and lifecycle controls --- action.yml | 3 + build/cli/index.js | 500 ++++++++++++++++-- .../src/actions/github_action_ai_inputs.d.ts | 2 + .../actions/local_action_configuration.d.ts | 1 + .../local_action_configuration_sections.d.ts | 1 + .../application/errors/application_error.d.ts | 13 + .../policies/action_summary_policy.d.ts | 1 + .../policies/lifecycle_state_policy.d.ts | 10 + .../policies/status_command_policy.d.ts | 26 + .../ports/execution_resolution_ports.d.ts | 1 + .../ports/pull_request_description_ports.d.ts | 7 + .../comment_automation_contracts.d.ts | 5 + .../usecases/issue_comment_use_case.d.ts | 4 +- .../pull_request_review_comment_use_case.d.ts | 4 +- ...ate_pull_request_description_use_case.d.ts | 2 + ...ate_pull_request_description_workflow.d.ts | 2 +- build/cli/src/cli/commands/reconcile.d.ts | 10 + build/cli/src/data/model/ai.d.ts | 5 +- build/cli/src/data/model/config.d.ts | 2 + .../cli/src/data/model/execution_inputs.d.ts | 22 + .../pull_request_lifecycle_repository.d.ts | 2 + build/cli/src/domain/copilot_command.d.ts | 2 +- .../src/domain/pull_request_description.d.ts | 13 + build/cli/src/domain/setup.d.ts | 3 + .../github_pull_request_provider_ports.d.ts | 11 + build/cli/src/utils/constants.d.ts | 1 + build/github_action/index.js | 476 +++++++++++++++-- .../src/actions/github_action_ai_inputs.d.ts | 2 + .../actions/local_action_configuration.d.ts | 1 + .../local_action_configuration_sections.d.ts | 1 + .../application/errors/application_error.d.ts | 13 + .../policies/action_summary_policy.d.ts | 1 + .../policies/lifecycle_state_policy.d.ts | 10 + .../policies/status_command_policy.d.ts | 26 + .../ports/execution_resolution_ports.d.ts | 1 + .../ports/pull_request_description_ports.d.ts | 7 + .../comment_automation_contracts.d.ts | 5 + .../usecases/issue_comment_use_case.d.ts | 4 +- .../pull_request_review_comment_use_case.d.ts | 4 +- ...ate_pull_request_description_use_case.d.ts | 2 + ...ate_pull_request_description_workflow.d.ts | 2 +- .../src/cli/commands/reconcile.d.ts | 10 + build/github_action/src/data/model/ai.d.ts | 5 +- .../github_action/src/data/model/config.d.ts | 2 + .../src/data/model/execution_inputs.d.ts | 22 + .../pull_request_lifecycle_repository.d.ts | 2 + .../src/domain/copilot_command.d.ts | 2 +- .../src/domain/pull_request_description.d.ts | 13 + build/github_action/src/domain/setup.d.ts | 3 + .../github_pull_request_provider_ports.d.ts | 11 + build/github_action/src/utils/constants.d.ts | 1 + docs/agents/cli-commands.mdx | 1 + docs/configuration.mdx | 1 + docs/pull-requests/ai-description.mdx | 16 + docs/pull-requests/configuration.mdx | 1 + docs/pull-requests/workflow-setup.mdx | 8 + docs/single-actions/workflow-and-cli.mdx | 13 + setup/workflows/copilot_commit.yml | 1 + setup/workflows/copilot_issue.yml | 1 + setup/workflows/copilot_issue_comment.yml | 1 + setup/workflows/copilot_pull_request.yml | 7 + .../copilot_pull_request_comment.yml | 1 + src/actions/github_action_ai_inputs.ts | 8 +- src/actions/github_action_completion.ts | 1 + src/actions/github_action_execution.ts | 1 + .../local_action_configuration_sections.ts | 7 +- src/actions/local_action_execution.ts | 3 +- src/application/errors/application_error.ts | 39 ++ .../__tests__/action_summary_policy.test.ts | 2 + .../__tests__/lifecycle_event_replay.test.ts | 37 ++ .../__tests__/lifecycle_state_policy.test.ts | 35 +- .../setup_configuration_policy.test.ts | 13 + .../__tests__/status_command_policy.test.ts | 58 ++ .../policies/action_summary_policy.ts | 2 + .../policies/agent_activity_policy.ts | 2 +- .../policies/lifecycle_state_policy.ts | 41 ++ .../lifecycle_waiting_state_policy.ts | 3 + .../policies/setup_configuration_policy.ts | 8 + .../policies/status_command_policy.ts | 107 ++++ .../ports/execution_resolution_ports.ts | 2 +- .../ports/pull_request_description_ports.ts | 8 + .../comment_automation_use_case.test.ts | 28 + ...nchronize_lifecycle_state_use_case.test.ts | 24 + .../synchronize_lifecycle_state_use_case.ts | 19 +- .../comment_automation_command_workflow.ts | 18 + .../usecases/comment_automation_contracts.ts | 6 + .../usecases/comment_automation_use_case.ts | 12 +- .../execution_issue_number_policy.ts | 8 +- .../usecases/issue_comment_use_case.ts | 3 + .../pull_request_review_comment_use_case.ts | 3 + .../usecases/pull_request_workflow.ts | 16 +- ...pdate_pull_request_description_use_case.ts | 10 + ...pdate_pull_request_description_workflow.ts | 67 ++- src/cli/command_registry.ts | 2 + src/cli/commands/reconcile.ts | 79 +++ src/cli/setup_config_file.ts | 2 +- src/cli/setup_prompt_adapter.ts | 5 + src/data/model/__tests__/pull_request.test.ts | 20 + src/data/model/ai.ts | 14 +- src/data/model/config.ts | 8 + src/data/model/execution_inputs.ts | 26 + src/data/model/pull_request.ts | 26 +- .../pull_request_lifecycle_repository.test.ts | 16 +- .../pull_request_lifecycle_repository.ts | 21 + src/domain/__tests__/copilot_command.test.ts | 2 + .../pull_request_description.test.ts | 24 + src/domain/copilot_command.ts | 1 + src/domain/pull_request_description.ts | 54 ++ src/domain/setup.ts | 3 + .../main_run_route_composition_root.test.ts | 2 + .../main_run_route_composition_root.ts | 46 +- .../github_pull_request_provider_ports.ts | 5 + .../configuration_payload_policy.ts | 2 + .../collect_architecture_metrics.test.ts | 2 +- src/utils/constants.ts | 1 + 115 files changed, 2131 insertions(+), 151 deletions(-) create mode 100644 build/cli/src/application/errors/application_error.d.ts create mode 100644 build/cli/src/application/policies/status_command_policy.d.ts create mode 100644 build/cli/src/cli/commands/reconcile.d.ts create mode 100644 build/cli/src/domain/pull_request_description.d.ts create mode 100644 build/github_action/src/application/errors/application_error.d.ts create mode 100644 build/github_action/src/application/policies/status_command_policy.d.ts create mode 100644 build/github_action/src/cli/commands/reconcile.d.ts create mode 100644 build/github_action/src/domain/pull_request_description.d.ts create mode 100644 src/application/errors/application_error.ts create mode 100644 src/application/policies/__tests__/lifecycle_event_replay.test.ts create mode 100644 src/application/policies/__tests__/status_command_policy.test.ts create mode 100644 src/application/policies/status_command_policy.ts create mode 100644 src/cli/commands/reconcile.ts create mode 100644 src/domain/__tests__/pull_request_description.test.ts create mode 100644 src/domain/pull_request_description.ts diff --git a/action.yml b/action.yml index fd758955..2fb4baba 100644 --- a/action.yml +++ b/action.yml @@ -535,6 +535,9 @@ inputs: ai-pull-request-description: description: "Enable AI-powered automatic updates for pull request descriptions." default: "true" + ai-pull-request-description-mode: + description: "PR description policy: replace (legacy full ownership), append (preserve human text), preserve (only explicit /copilot description), or disabled." + default: "replace" ai-ignore-files: description: "Comma-separated list of files to ignore for AI operations." default: "" diff --git a/build/cli/index.js b/build/cli/index.js index 11b41f25..a4f16dca 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -54896,6 +54896,7 @@ const input_number_policy_1 = __nccwpck_require__(47165); const input_values_policy_1 = __nccwpck_require__(68841); const agent_input_builder_1 = __nccwpck_require__(71404); const image_configuration_builder_1 = __nccwpck_require__(9246); +const pull_request_description_1 = __nccwpck_require__(45315); function input(additionalParams, actionInputs, key) { return (0, action_input_source_1.resolveActionInput)(additionalParams, actionInputs, key); } @@ -54916,10 +54917,14 @@ function readLocalCoreConfiguration(additionalParams, actionInputs) { function readLocalAgentConfiguration(additionalParams, actionInputs) { const agentTasks = (0, agent_input_builder_1.buildAgentTasksFromValues)({ ...actionInputs, ...additionalParams }); const bugbotFixVerifyCommandsInput = input(additionalParams, actionInputs, constants_1.INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) ?? ''; + const pullRequestDescription = (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); return { agentTasks, agentModel: agentTasks.findings.model, - aiPullRequestDescription: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)), + aiPullRequestDescription: pullRequestDescription, + aiPullRequestDescriptionMode: pullRequestDescription + ? (0, pull_request_description_1.normalizePullRequestDescriptionMode)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) + : 'disabled', aiMembersOnly: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_MEMBERS_ONLY)), aiIncludeReasoning: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_INCLUDE_REASONING)), aiIgnoreFilesInput: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_IGNORE_FILES), @@ -55126,7 +55131,7 @@ const configuration_builders_1 = __nccwpck_require__(19094); const branches_builder_1 = __nccwpck_require__(30085); const size_threshold_builder_1 = __nccwpck_require__(39757); function buildLocalActionExecution(configuration, additionalParams) { - const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, aiPullRequestDescription, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks, branchManagementLauncherLabel, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, } = configuration; + const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, aiPullRequestDescription, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks, branchManagementLauncherLabel, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, } = configuration; return (0, execution_builder_1.buildExecution)({ debug, singleAction: new single_action_1.SingleAction(singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog), @@ -55143,7 +55148,7 @@ function buildLocalActionExecution(configuration, additionalParams) { commit: imageConfiguration.commit, }), tokens: (0, configuration_builders_1.buildTokens)(token), - ai: new ai_1.Ai('', agentModel, aiPullRequestDescription, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks), + ai: new ai_1.Ai('', agentModel, aiPullRequestDescription, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks, aiPullRequestDescriptionMode), labels: (0, configuration_builders_1.buildLabels)({ branching: { launcher: branchManagementLauncherLabel }, workflow: { bug: bugLabel, bugfix: bugfixLabel, hotfix: hotfixLabel, enhancement: enhancementLabel, feature: featureLabel, release: releaseLabel, question: questionLabel, help: helpLabel, deploy: deployLabel, deployed: deployedLabel, docs: docsLabel, documentation: documentationLabel, chore: choreLabel, maintenance: maintenanceLabel }, @@ -55560,6 +55565,34 @@ function resolveWorkflowIdentifier(workflowRef) { } +/***/ }), + +/***/ 75999: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ApplicationError = void 0; +exports.toApplicationError = toApplicationError; +/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ +class ApplicationError extends Error { + constructor(message, kind = 'unknown', options = {}) { + super(message); + this.name = 'ApplicationError'; + this.kind = kind; + this.retryable = options.retryable ?? false; + this.cause = options.cause; + } +} +exports.ApplicationError = ApplicationError; +function toApplicationError(error, message, kind = 'unknown', options = {}) { + return error instanceof ApplicationError + ? error + : new ApplicationError(message, kind, { ...options, cause: error }); +} + + /***/ }), /***/ 79966: @@ -55631,7 +55664,7 @@ function hasComment(execution) { return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; } function hasTarget(execution) { - if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + if (['pull_request', 'pull_request_review', 'pull_request_review_comment', 'check_suite', 'workflow_run'].includes(execution.eventName)) { return execution.pullRequest.number > 0; } return execution.issue.number > 0 || execution.issueNumber > 0; @@ -56679,6 +56712,7 @@ exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; exports.buildSetupActionInputs = buildSetupActionInputs; const agent_1 = __nccwpck_require__(89040); const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); +const pull_request_description_1 = __nccwpck_require__(45315); exports.SETUP_AGENT_TASKS = [ 'planner', 'findings', @@ -56759,6 +56793,7 @@ function createDefaultSetupConfiguration() { }, ai: { pullRequestDescription: true, + pullRequestDescriptionMode: 'replace', ignoreFiles: 'build/*', membersOnly: false, includeReasoning: true, @@ -56828,6 +56863,10 @@ function validateSetupConfiguration(configuration) { if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { errors.push('Bugbot severity must be info, low, medium, or high.'); } + if (configuration.ai.pullRequestDescriptionMode !== undefined + && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { + errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); + } if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } @@ -56933,6 +56972,7 @@ function buildSetupRepositoryVariables(configuration) { add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); + add('AI_PULL_REQUEST_DESCRIPTION_MODE', configuration.ai.pullRequestDescriptionMode); add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); @@ -56968,6 +57008,7 @@ function buildSetupActionInputs(configuration) { 'pull-requests-locale': repository.pullRequestLocale, 'commit-prefix-transforms': repository.commitPrefixTransforms, 'ai-pull-request-description': String(ai.pullRequestDescription), + 'ai-pull-request-description-mode': (0, pull_request_description_1.normalizePullRequestDescriptionMode)(ai.pullRequestDescriptionMode), 'ai-ignore-files': ai.ignoreFiles, 'ai-members-only': String(ai.membersOnly), 'ai-include-reasoning': String(ai.includeReasoning), @@ -57026,6 +57067,103 @@ function unique(values) { } +/***/ }), + +/***/ 3449: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildCopilotStatusSnapshot = buildCopilotStatusSnapshot; +exports.buildCopilotStatusResult = buildCopilotStatusResult; +exports.formatCopilotStatus = formatCopilotStatus; +const result_1 = __nccwpck_require__(73817); +/** Builds a read-only status snapshot from the facts already loaded by setup. */ +function buildCopilotStatusSnapshot(execution) { + const issueLabels = [...(execution.labels?.currentIssueLabels ?? [])]; + const pullRequestLabels = [...(execution.labels?.currentPullRequestLabels ?? [])]; + const isPullRequestTarget = execution.isPullRequest || execution.pullRequest?.number > 0 || execution.pullRequest?.isPullRequestReviewComment; + const targetLabels = isPullRequestTarget ? pullRequestLabels : issueLabels; + const lifecycleLabels = execution.labels?.lifecycle ?? {}; + const lifecycle = Object.entries({ + planned: lifecycleLabels.planned, + 'in-progress': lifecycleLabels.inProgress, + reviewing: lifecycleLabels.reviewing, + 'changes-requested': lifecycleLabels.changesRequested, + verified: lifecycleLabels.verified, + ready: lifecycleLabels.ready, + blocked: lifecycleLabels.blocked, + }).find(([, label]) => label && targetLabels.includes(label))?.[0]; + const waitingFor = Object.entries({ + maintainer: lifecycleLabels.awaitingMaintainer, + 'issue-author': lifecycleLabels.awaitingIssueAuthor, + }).find(([, label]) => label && targetLabels.includes(label))?.[0]; + const findingStates = execution.currentConfiguration?.results + ?.map(result => (0, result_1.getResultPayload)(result.payload)?.findingStates) + .find(isFindingStateCounts); + return { + owner: execution.owner, + repository: execution.repo, + event: execution.eventName || 'unknown', + action: execution.inputs?.action ?? '', + target: execution.pullRequest?.number > 0 || execution.pullRequest?.isPullRequestReviewComment + ? 'pull-request' + : execution.isPush + ? 'push' + : execution.issue?.number > 0 || execution.isIssue + ? 'issue' + : 'repository', + ...(execution.issue?.number > 0 ? { issueNumber: execution.issue.number } : {}), + ...(execution.pullRequest?.number > 0 ? { pullRequestNumber: execution.pullRequest.number } : {}), + ...(execution.commit?.branch ? { branch: execution.commit.branch } : {}), + ...(lifecycle ? { lifecycle } : {}), + ...(waitingFor ? { waitingFor } : {}), + issueLabels, + pullRequestLabels, + ...(findingStates ? { activeFindings: findingStates } : {}), + pullRequestDescriptionMode: execution.ai.getPullRequestDescriptionMode?.() + ?? (execution.ai.getAiPullRequestDescription() ? 'replace' : 'disabled'), + }; +} +function buildCopilotStatusResult(execution, taskId) { + const snapshot = buildCopilotStatusSnapshot(execution); + return new result_1.Result({ + id: `${taskId}.Status`, + success: true, + executed: true, + stepFormat: 'markdown', + steps: [formatCopilotStatus(snapshot)], + payload: { status: snapshot }, + }); +} +function formatCopilotStatus(snapshot) { + const lines = [ + '## Copilot status', + `- **Repository:** ${snapshot.owner}/${snapshot.repository}`, + `- **Target:** ${snapshot.target}${snapshot.issueNumber ? ` #${snapshot.issueNumber}` : ''}${snapshot.pullRequestNumber ? ` / PR #${snapshot.pullRequestNumber}` : ''}`, + `- **Event:** ${snapshot.event}${snapshot.action ? ` (${snapshot.action})` : ''}`, + `- **Branch:** ${snapshot.branch ?? 'unknown'}`, + `- **Lifecycle:** ${snapshot.lifecycle ?? 'not set'}`, + `- **Waiting for:** ${snapshot.waitingFor ?? 'no pending human response'}`, + `- **PR description policy:** ${snapshot.pullRequestDescriptionMode}`, + `- **Issue labels:** ${snapshot.issueLabels.length > 0 ? snapshot.issueLabels.join(', ') : 'none'}`, + `- **PR labels:** ${snapshot.pullRequestLabels.length > 0 ? snapshot.pullRequestLabels.join(', ') : 'none'}`, + ]; + if (snapshot.activeFindings) { + lines.push(`- **Bugbot findings:** ${snapshot.activeFindings.open} open, ${snapshot.activeFindings.reopened} reopened, ${snapshot.activeFindings.resolved} resolved`); + } + return lines.join('\n'); +} +function isFindingStateCounts(value) { + return typeof value === 'object' + && value !== null + && typeof value.open === 'number' + && typeof value.reopened === 'number' + && typeof value.resolved === 'number'; +} + + /***/ }), /***/ 43193: @@ -58553,16 +58691,32 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; const result_1 = __nccwpck_require__(73817); +const status_command_policy_1 = __nccwpck_require__(3449); /** Executes deterministic /copilot commands without routing them through intent detection. */ async function runExplicitCommentCommand(param, options, command, actorAuthorizationPort) { + if (command.name === 'status') + return [(0, status_command_policy_1.buildCopilotStatusResult)(param, options.taskId)]; if (command.name === 'dismiss') return runDismissCommand(param, options, command, actorAuthorizationPort); + if (command.name === 'description') + return runDescriptionCommand(param, options); if (['review', 'findings', 'recheck'].includes(command.name)) return runReviewCommand(param, options, command); if (command.name === 'fix') return undefined; return runThinkCommand(param, options, command); } +async function runDescriptionCommand(param, options) { + if (!options.updatePullRequestDescriptionUseCase) { + return [new result_1.Result({ + id: `${options.taskId}.Description`, + success: false, + executed: false, + errors: ['Explicit pull-request description command is not available in this composition.'], + })]; + } + return options.updatePullRequestDescriptionUseCase.invokeExplicit(param); +} async function runDismissCommand(param, options, command, actorAuthorizationPort) { const allowed = await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.actor, param.tokens.token); if (!allowed || !options.dismissBugbotFindingsUseCase) { @@ -58739,12 +58893,7 @@ const logging_ports_1 = __nccwpck_require__(6152); const copilot_command_1 = __nccwpck_require__(11771); const comment_automation_command_workflow_1 = __nccwpck_require__(63134); const comment_automation_natural_language_workflow_1 = __nccwpck_require__(10554); -class CommentAutomationError extends Error { - constructor() { - super("Comment automation failed."); - this.name = "CommentAutomationError"; - } -} +const application_error_1 = __nccwpck_require__(75999); async function runCommentAutomation(param, options, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts) { (0, logging_ports_1.logInfo)(`${options.taskId} started.`); let languageResults = []; @@ -58764,8 +58913,8 @@ async function runCommentAutomation(param, options, actorAuthorizationPort, auth bugbotResolutionPorts, }); } - catch { - const error = new CommentAutomationError(); + catch (cause) { + const error = new application_error_1.ApplicationError("Comment automation failed.", 'workflow', { cause }); (0, logging_ports_1.logError)(error); return [...languageResults, new result_1.Result({ id: options.taskId, @@ -58918,8 +59067,13 @@ const title_utils_1 = __nccwpck_require__(46267); function resolveEventIssueNumber(execution) { if (execution.isIssue) return positiveIssueNumberOrUndefined(execution.issue.number); - if (execution.isPullRequest) - return positiveIssueNumberOrUndefined((0, title_utils_1.extractIssueNumberFromBranch)(execution.pullRequest.head)); + if (execution.isPullRequest) { + if (['check_suite', 'workflow_run'].includes(String(execution.inputs?.eventName ?? ''))) { + return positiveIssueNumberOrUndefined(execution.pullRequest.number); + } + return positiveIssueNumberOrUndefined((0, title_utils_1.extractIssueNumberFromBranch)(execution.pullRequest.head)) + ?? positiveIssueNumberOrUndefined(execution.pullRequest.number); + } if (execution.isPush) return positiveIssueNumberOrUndefined((0, title_utils_1.extractIssueNumberFromPush)(execution.commit.branch)); return positiveIssueNumberOrUndefined(execution.issueNumber); @@ -59137,7 +59291,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.IssueCommentUseCase = void 0; const comment_automation_use_case_1 = __nccwpck_require__(9661); class IssueCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -59150,6 +59304,7 @@ class IssueCommentUseCase { this.gitCommitPort = gitCommitPort; this.dismissBugbotFindingsUseCase = dismissBugbotFindingsUseCase; this.reviewPotentialProblemsUseCase = reviewPotentialProblemsUseCase; + this.updatePullRequestDescriptionUseCase = updatePullRequestDescriptionUseCase; this.taskId = "IssueCommentUseCase"; } async invoke(param) { @@ -59164,6 +59319,7 @@ class IssueCommentUseCase { gitCommitPort: this.gitCommitPort, dismissBugbotFindingsUseCase: this.dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase: this.reviewPotentialProblemsUseCase, + updatePullRequestDescriptionUseCase: this.updatePullRequestDescriptionUseCase, }, this.actorAuthorizationPort, this.authenticatedUserPort, this.bugbotResolutionPorts); } } @@ -59282,7 +59438,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewCommentUseCase = void 0; const comment_automation_use_case_1 = __nccwpck_require__(9661); class PullRequestReviewCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -59295,6 +59451,7 @@ class PullRequestReviewCommentUseCase { this.gitCommitPort = gitCommitPort; this.dismissBugbotFindingsUseCase = dismissBugbotFindingsUseCase; this.reviewPotentialProblemsUseCase = reviewPotentialProblemsUseCase; + this.updatePullRequestDescriptionUseCase = updatePullRequestDescriptionUseCase; this.taskId = "PullRequestReviewCommentUseCase"; } async invoke(param) { @@ -59309,6 +59466,7 @@ class PullRequestReviewCommentUseCase { gitCommitPort: this.gitCommitPort, dismissBugbotFindingsUseCase: this.dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase: this.reviewPotentialProblemsUseCase, + updatePullRequestDescriptionUseCase: this.updatePullRequestDescriptionUseCase, }, this.actorAuthorizationPort, this.authenticatedUserPort, this.bugbotResolutionPorts); } } @@ -59357,6 +59515,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runPullRequestWorkflow = runPullRequestWorkflow; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); +const application_error_1 = __nccwpck_require__(75999); /** Coordinates pull-request lifecycle actions while preserving their sequential order. */ async function runPullRequestWorkflow(param, taskId, ports) { try { @@ -59372,14 +59531,14 @@ async function runPullRequestWorkflow(param, taskId, ports) { ports.workflowSteps.checkPriorityPullRequestSize, ]; const results = await runSteps(param, steps); - if (param.ai.getAiPullRequestDescription()) { + if (shouldUpdatePullRequestDescriptionAutomatically(param)) { results.push(...(await ports.updatePullRequestDescriptionUseCase.invoke(param))); } results.push(...(await runPullRequestReview(param, ports))); return results; } if (param.pullRequest.isSynchronize) { - const results = param.ai.getAiPullRequestDescription() + const results = shouldUpdatePullRequestDescriptionAutomatically(param) ? await ports.updatePullRequestDescriptionUseCase.invoke(param) : []; results.push(...(await runPullRequestReview(param, ports))); @@ -59389,8 +59548,8 @@ async function runPullRequestWorkflow(param, taskId, ports) { return ports.workflowSteps.closeIssueAfterMerging.invoke(param); } } - catch { - const semanticError = new Error("Unable to process the pull request."); + catch (cause) { + const semanticError = new application_error_1.ApplicationError("Unable to process the pull request.", 'workflow', { cause }); (0, logging_ports_1.logError)(semanticError); return [ new result_1.Result({ @@ -59404,6 +59563,12 @@ async function runPullRequestWorkflow(param, taskId, ports) { } return []; } +function shouldUpdatePullRequestDescriptionAutomatically(param) { + const mode = param.ai.getPullRequestDescriptionMode?.(); + return mode === undefined + ? param.ai.getAiPullRequestDescription() + : mode === 'replace' || mode === 'append'; +} async function runPullRequestReview(param, ports) { if (!ports.reviewPotentialProblemsUseCase || !shouldReviewPullRequest(param)) return []; @@ -65407,6 +65572,15 @@ class UpdatePullRequestDescriptionUseCase { aiRepository: this.aiRepository, }); } + /** Explicit comment commands may update a preserved PR body on demand. */ + async invokeExplicit(param) { + return await (0, update_pull_request_description_workflow_1.runUpdatePullRequestDescriptionWorkflow)(param, this.taskId, { + pullRequestDescriptionCommandPort: this.pullRequestDescriptionCommandPort, + issueDescriptionQueryPort: this.issueDescriptionQueryPort, + organizationMembersPort: this.organizationMembersPort, + aiRepository: this.aiRepository, + }, true); + } } exports.UpdatePullRequestDescriptionUseCase = UpdatePullRequestDescriptionUseCase; @@ -65427,11 +65601,15 @@ const logging_ports_1 = __nccwpck_require__(6152); const project_context_instruction_1 = __nccwpck_require__(63907); const task_emoji_1 = __nccwpck_require__(46103); const github_comment_publication_policy_1 = __nccwpck_require__(72712); +const pull_request_description_1 = __nccwpck_require__(45315); +const application_error_1 = __nccwpck_require__(75999); /** Generates and publishes a PR description while keeping provider details behind ports. */ -async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependencies) { +async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependencies, force = false) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(taskId)} Executing ${taskId} (AI PR description).`); try { - const branches = getPullRequestBranches(param); + const pullRequestNumber = getPullRequestNumber(param); + const details = await loadPullRequestDetails(param, dependencies, pullRequestNumber, force); + const branches = getPullRequestBranches(param, details); if (!branches) { return [ new result_1.Result({ @@ -65444,6 +65622,10 @@ async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependenci }), ]; } + const mode = getPullRequestDescriptionMode(param); + if (mode === 'disabled' || (!force && !(0, pull_request_description_1.shouldAutomaticallyUpdatePullRequestDescription)(mode))) { + return skipped(taskId, `Automatic PR description updates are disabled by the "${mode}" mode.`); + } (0, logging_ports_1.logDebugInfo)(`PR description will be generated from workspace diff: base "${branches.baseBranch}", head "${branches.headBranch}" (configured agent will run git diff).`); const issueDescription = param.issueNumber > 0 ? (await dependencies.issueDescriptionQueryPort.getDescription(param.owner, param.repo, param.issueNumber, param.tokens.token)) ?? '' @@ -65473,31 +65655,54 @@ async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependenci agentId: agent_task_policy_1.AGENT_PLAN, prompt, }); - const pullRequestBody = (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(extractDescription(response)); + const generatedDescription = (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(extractDescription(response)); + const pullRequestBody = mode === 'replace' + ? generatedDescription + : (0, pull_request_description_1.mergeManagedPullRequestDescription)(details?.body ?? param.pullRequest.body, generatedDescription); (0, logging_ports_1.logDebugInfo)(`UpdatePullRequestDescription: agent response received. Description length=${pullRequestBody.length}.`); if (!pullRequestBody.trim()) { return newResult(taskId, false, true, ['Configured agent did not return a PR description.']); } - await dependencies.pullRequestDescriptionCommandPort.updateDescription(param.owner, param.repo, param.pullRequest.number, pullRequestBody, param.tokens.token); + await dependencies.pullRequestDescriptionCommandPort.updateDescription(param.owner, param.repo, pullRequestNumber, pullRequestBody, param.tokens.token); return [new result_1.Result({ id: taskId, success: true, executed: true, steps: [] })]; } - catch (error) { + catch (cause) { + const error = new application_error_1.ApplicationError('Unable to update pull request description.', 'workflow', { cause }); (0, logging_ports_1.logError)(error); return [ new result_1.Result({ id: taskId, success: false, executed: true, - steps: [`Error updating pull request description: ${error}`], + steps: [error.message], + errors: [error], }), ]; } } -function getPullRequestBranches(param) { - const headBranch = param.pullRequest.head; - const baseBranch = param.pullRequest.base; +function getPullRequestBranches(param, details) { + const headBranch = param.pullRequest.head || details?.headBranch; + const baseBranch = param.pullRequest.base || details?.baseBranch; return headBranch && baseBranch ? { headBranch, baseBranch } : undefined; } +function getPullRequestNumber(param) { + return param.pullRequest.number > 0 ? param.pullRequest.number : param.issue.number; +} +async function loadPullRequestDetails(param, dependencies, pullRequestNumber, force) { + if (pullRequestNumber <= 0 || !dependencies.pullRequestDescriptionCommandPort.getDetails) + return undefined; + const needsRemoteDetails = param.eventName === 'issue_comment' + || force + || !param.pullRequest.head + || !param.pullRequest.base; + if (!needsRemoteDetails) + return undefined; + return dependencies.pullRequestDescriptionCommandPort.getDetails(param.owner, param.repo, pullRequestNumber, param.tokens.token); +} +function getPullRequestDescriptionMode(param) { + return param.ai.getPullRequestDescriptionMode?.() + ?? (param.ai.getAiPullRequestDescription() ? 'replace' : 'disabled'); +} function extractDescription(response) { if (typeof response === 'string') return response; @@ -65778,6 +65983,7 @@ const detect_potential_problems_1 = __nccwpck_require__(70850); const setup_1 = __nccwpck_require__(32139); const upgrade_1 = __nccwpck_require__(27087); const doctor_1 = __nccwpck_require__(74364); +const reconcile_1 = __nccwpck_require__(4718); function registerCliCommands(program) { (0, think_1.registerThinkCommand)(program); (0, do_1.registerDoCommand)(program); @@ -65787,6 +65993,7 @@ function registerCliCommands(program) { (0, setup_1.registerSetupCommand)(program); (0, upgrade_1.registerUpgradeCommand)(program); (0, doctor_1.registerDoctorCommand)(program); + (0, reconcile_1.registerReconcileCommand)(program); return program; } @@ -66320,6 +66527,86 @@ function registerRecommendStepsCommand(program) { } +/***/ }), + +/***/ 4718: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.registerReconcileCommand = registerReconcileCommand; +exports.runReconcileCommand = runReconcileCommand; +const cli_context_1 = __nccwpck_require__(21307); +const setup_configuration_policy_1 = __nccwpck_require__(56637); +const setup_config_file_1 = __nccwpck_require__(11196); +const setup_workspace_adapter_1 = __nccwpck_require__(5729); +/** Reconciles setup-managed workflow files locally; remote GitHub state is never changed. */ +function registerReconcileCommand(program) { + program + .command('reconcile') + .description('Detect setup drift and optionally reconcile setup-managed workflow files') + .option('--config ', 'YAML or JSON setup configuration used as the expected contract') + .option('--apply', 'Apply local workflow/template reconciliation after showing the drift') + .option('--json', 'Print a machine-readable reconciliation report') + .action((options) => runReconcileCommand(options)); +} +function runReconcileCommand(options, workspace = new setup_workspace_adapter_1.SetupWorkspaceAdapter()) { + const cwd = process.cwd(); + if (!(0, cli_context_1.isInsideGitRepo)(cwd)) + throw new Error('Run "copilot reconcile" from the root of a git repository.'); + const gitInfo = (0, cli_context_1.getGitInfo)(); + if ('error' in gitInfo) + throw new Error(gitInfo.error); + const overrides = options.config ? (0, setup_config_file_1.loadSetupConfigurationOverrides)(options.config) : {}; + const configuration = (0, setup_configuration_policy_1.mergeSetupConfiguration)((0, setup_configuration_policy_1.createDefaultSetupConfiguration)(), overrides); + const comparisons = [...(workspace.compareWorkflows?.(configuration.features) ?? [])]; + const drift = comparisons.filter(comparison => comparison.status !== 'unchanged'); + const report = { + repository: `${gitInfo.owner}/${gitInfo.repo}`, + scope: 'setup-workflows', + driftDetected: drift.length > 0, + applied: false, + files: comparisons, + result: undefined, + }; + if (options.apply && drift.length > 0) { + report.result = workspace.prepare({ + features: configuration.features, + updateExistingWorkflows: true, + approvedWorkflowFiles: drift.map(comparison => comparison.file), + }); + report.applied = true; + } + if (options.json) { + console.log(JSON.stringify(report, null, 2)); + } + else { + console.log(`🔎 Reconciling ${report.scope} for ${report.repository}...`); + if (comparisons.length === 0) + console.log(' No setup-managed workflows were found in the package contract.'); + for (const comparison of comparisons) { + const icon = comparison.status === 'unchanged' ? '✅' : comparison.status === 'missing' ? '❌' : '⚠️'; + console.log(` ${icon} ${comparison.destination} (${comparison.status})`); + } + if (report.result) + console.log(`✅ Reconciliation applied: ${report.result.copied} copied, ${report.result.skipped} skipped.`); + } + if (report.applied) { + process.exitCode = 0; + return; + } + if (drift.length > 0) { + if (!options.json) + console.log('ℹ️ Run with --apply to reconcile the local setup-managed files.'); + process.exitCode = 1; + } + else { + process.exitCode = 0; + } +} + + /***/ }), /***/ 32139: @@ -66752,7 +67039,7 @@ const REPOSITORY_STRING_KEYS = new Set([ const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush']); const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout']); const AI_BOOLEAN_KEYS = new Set(['pullRequestDescription', 'membersOnly', 'includeReasoning']); -const AI_STRING_KEYS = new Set(['ignoreFiles', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); +const AI_STRING_KEYS = new Set(['ignoreFiles', 'pullRequestDescriptionMode', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); const AI_NUMBER_KEYS = new Set(['bugbotCommentLimit']); const PROJECT_KEYS = new Set([ 'ids', @@ -66933,6 +67220,7 @@ class SetupPromptAdapter { console.log(color('\n4. Configure AI, projects, and release safety\n', 36)); const ai = defaults.ai; ai.pullRequestDescription = await this.askBoolean('Generate AI pull-request descriptions?', ai.pullRequestDescription); + ai.pullRequestDescriptionMode = await this.askChoice('Pull-request description mode', ['replace', 'append', 'preserve', 'disabled'], ai.pullRequestDescriptionMode ?? 'replace'); ai.ignoreFiles = await this.askText('AI ignore file patterns (comma-separated)', ai.ignoreFiles); ai.membersOnly = await this.askBoolean('Restrict AI processing to repository members?', ai.membersOnly); ai.includeReasoning = await this.askBoolean('Include agent reasoning where supported?', ai.includeReasoning); @@ -67250,11 +67538,12 @@ Object.defineProperty(exports, "isAgentConfigurationReady", ({ enumerable: true, Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Ai = void 0; const agent_command_1 = __nccwpck_require__(77923); +const pull_request_description_1 = __nccwpck_require__(45315); class Ai { constructor(_configurationSource, model, aiPullRequestDescription, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotMinSeverity, bugbotCommentLimit, bugbotFixVerifyCommands = [], agentTasks = { findings: { provider: 'codex', modelProvider: 'openai', model, command: (0, agent_command_1.defaultAgentCommand)({ provider: 'codex', modelProvider: 'openai', model }) }, fixer: { provider: 'codex', modelProvider: 'openai', model, command: (0, agent_command_1.defaultAgentCommand)({ provider: 'codex', modelProvider: 'openai', model }) }, - }) { + }, pullRequestDescriptionMode = pull_request_description_1.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE) { this.aiPullRequestDescription = aiPullRequestDescription; this.aiMembersOnly = aiMembersOnly; this.aiIgnoreFiles = aiIgnoreFiles; @@ -67263,10 +67552,14 @@ class Ai { this.bugbotCommentLimit = bugbotCommentLimit; this.bugbotFixVerifyCommands = bugbotFixVerifyCommands; this.agentTasks = agentTasks; + this.pullRequestDescriptionMode = (0, pull_request_description_1.normalizePullRequestDescriptionMode)(pullRequestDescriptionMode); } getAiPullRequestDescription() { return this.aiPullRequestDescription; } + getPullRequestDescriptionMode() { + return this.pullRequestDescriptionMode; + } getAiMembersOnly() { return this.aiMembersOnly; } @@ -67409,14 +67702,20 @@ exports.Commit = Commit; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.Config = void 0; +exports.Config = exports.CONFIG_SCHEMA_VERSION = void 0; const branch_configuration_1 = __nccwpck_require__(71934); const recommendation_state_1 = __nccwpck_require__(68514); const model_input_1 = __nccwpck_require__(14637); +exports.CONFIG_SCHEMA_VERSION = 1; class Config { constructor(data) { this.results = []; const input = (0, model_input_1.asModelInput)(data); + this.schemaVersion = typeof input.schemaVersion === 'number' + && Number.isInteger(input.schemaVersion) + && input.schemaVersion > 0 + ? input.schemaVersion + : exports.CONFIG_SCHEMA_VERSION; this.branchType = (0, model_input_1.readString)(input, 'branchType'); this.hotfixOriginBranch = (0, model_input_1.readOptionalString)(input, 'hotfixOriginBranch'); this.hotfixBranch = (0, model_input_1.readOptionalString)(input, 'hotfixBranch'); @@ -68229,7 +68528,11 @@ class PullRequest { return this.inputs?.pull_request?.user?.login ?? ''; } get number() { - return (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.inputs?.pull_request?.number) ?? -1; + return (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.inputs?.pull_request?.number) + ?? (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.inputs?.review?.pull_request?.number) + ?? uniquePullRequestNumber(this.inputs?.check_suite?.pull_requests) + ?? uniquePullRequestNumber(this.inputs?.workflow_run?.pull_requests) + ?? -1; } get url() { return this.inputs?.pull_request?.html_url ?? ''; @@ -68238,7 +68541,10 @@ class PullRequest { return this.inputs?.pull_request?.body ?? ''; } get head() { - return this.inputs?.pull_request?.head?.ref ?? ''; + return this.inputs?.pull_request?.head?.ref + ?? this.inputs?.check_suite?.head_branch + ?? this.inputs?.workflow_run?.head_branch + ?? ''; } get base() { return this.inputs?.pull_request?.base?.ref ?? ''; @@ -68261,7 +68567,12 @@ class PullRequest { return this.action === 'synchronize'; } get isPullRequest() { - return this.inputs?.eventName === 'pull_request'; + return [ + 'pull_request', + 'pull_request_review', + 'check_suite', + 'workflow_run', + ].includes(this.inputs?.eventName ?? ''); } get isPullRequestReviewComment() { return this.inputs?.eventName === 'pull_request_review_comment'; @@ -68296,6 +68607,11 @@ class PullRequest { } } exports.PullRequest = PullRequest; +function uniquePullRequestNumber(pullRequests) { + return pullRequests?.length === 1 + ? (0, positive_integer_policy_1.parsePositiveSafeInteger)(pullRequests[0]?.number) + : undefined; +} /***/ }), @@ -72477,6 +72793,21 @@ class PullRequestLifecycleRepository { }); (0, logger_1.logDebugInfo)(`Updated PR #${pullRequestNumber} description with: ${description}`); }; + this.getDetails = async (owner, repository, pullRequestNumber, token) => { + const octokit = this.githubClient.getClient(token); + if (!octokit.rest.pulls.get) + throw new Error('Pull-request details query is not available.'); + const { data } = await octokit.rest.pulls.get({ + owner, + repo: repository, + pull_number: pullRequestNumber, + }); + return { + body: data.body ?? '', + headBranch: data.head?.ref ?? '', + baseBranch: data.base?.ref ?? '', + }; + }; } async listOpenPullRequests(octokit, owner, repository, filters = {}) { const allPullRequests = []; @@ -73851,6 +74182,7 @@ exports.COPILOT_COMMAND_NAMES = [ 'estimate', 'test-plan', 'status', + 'description', 'review', 'findings', 'fix', @@ -74061,6 +74393,65 @@ function parsePositiveSafeInteger(value) { } +/***/ }), + +/***/ 45315: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.MANAGED_PULL_REQUEST_DESCRIPTION_END = exports.MANAGED_PULL_REQUEST_DESCRIPTION_START = exports.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE = exports.PULL_REQUEST_DESCRIPTION_MODES = void 0; +exports.normalizePullRequestDescriptionMode = normalizePullRequestDescriptionMode; +exports.hasManagedPullRequestDescription = hasManagedPullRequestDescription; +exports.renderManagedPullRequestDescription = renderManagedPullRequestDescription; +exports.mergeManagedPullRequestDescription = mergeManagedPullRequestDescription; +exports.shouldAutomaticallyUpdatePullRequestDescription = shouldAutomaticallyUpdatePullRequestDescription; +exports.PULL_REQUEST_DESCRIPTION_MODES = [ + 'replace', + 'append', + 'preserve', + 'disabled', +]; +exports.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE = 'replace'; +exports.MANAGED_PULL_REQUEST_DESCRIPTION_START = ''; +exports.MANAGED_PULL_REQUEST_DESCRIPTION_END = ''; +/** Normalizes public configuration while keeping invalid values safe and backwards compatible. */ +function normalizePullRequestDescriptionMode(value) { + const normalized = String(value ?? '').trim().toLowerCase(); + return exports.PULL_REQUEST_DESCRIPTION_MODES.includes(normalized) + ? normalized + : exports.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE; +} +function hasManagedPullRequestDescription(body) { + return typeof body === 'string' && body.includes(exports.MANAGED_PULL_REQUEST_DESCRIPTION_START); +} +/** Renders one bounded Copilot-owned section without taking ownership of the rest of the body. */ +function renderManagedPullRequestDescription(generated) { + return [ + exports.MANAGED_PULL_REQUEST_DESCRIPTION_START, + generated.trim(), + exports.MANAGED_PULL_REQUEST_DESCRIPTION_END, + ].join('\n'); +} +/** Replaces the existing managed section, or appends one when none exists. */ +function mergeManagedPullRequestDescription(currentBody, generated) { + const current = typeof currentBody === 'string' ? currentBody.trim() : ''; + const managed = renderManagedPullRequestDescription(generated); + const start = current.indexOf(exports.MANAGED_PULL_REQUEST_DESCRIPTION_START); + const end = current.indexOf(exports.MANAGED_PULL_REQUEST_DESCRIPTION_END, start + exports.MANAGED_PULL_REQUEST_DESCRIPTION_START.length); + if (start >= 0 && end >= start) { + const before = current.slice(0, start).trimEnd(); + const after = current.slice(end + exports.MANAGED_PULL_REQUEST_DESCRIPTION_END.length).trimStart(); + return [before, managed, after].filter(Boolean).join('\n\n').trim(); + } + return current ? `${current}\n\n${managed}` : managed; +} +function shouldAutomaticallyUpdatePullRequestDescription(mode) { + return mode === 'replace' || mode === 'append'; +} + + /***/ }), /***/ 67057: @@ -74946,7 +75337,6 @@ const check_pull_request_comment_language_use_case_1 = __nccwpck_require__(21729 const comment_language_translation_workflow_1 = __nccwpck_require__(72770); const branch_compare_repository_1 = __nccwpck_require__(95859); const merge_repository_1 = __nccwpck_require__(31412); -const pull_request_lifecycle_repository_1 = __nccwpck_require__(24189); const repository_release_publication_repository_1 = __nccwpck_require__(42075); const repository_tag_repository_1 = __nccwpck_require__(58717); const git_commit_adapter_1 = __nccwpck_require__(18606); @@ -74964,6 +75354,9 @@ const issue_interaction_composition_root_1 = __nccwpck_require__(92503); const issue_labels_composition_root_1 = __nccwpck_require__(34780); const issue_use_case_composition_root_1 = __nccwpck_require__(43022); const pull_request_use_case_composition_root_1 = __nccwpck_require__(70636); +const organization_members_composition_root_1 = __nccwpck_require__(50603); +const update_pull_request_description_use_case_1 = __nccwpck_require__(75089); +const pull_request_lifecycle_repository_1 = __nccwpck_require__(24189); function createDetectPotentialProblemsUseCase() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); return new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)(), bugbot.context, bugbot.publication, bugbot.resolution); @@ -74980,7 +75373,8 @@ function createIssueCommentUseCaseCompositionRoot() { const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); - return new issue_comment_use_case_1.IssueCommentUseCase(new check_issue_comment_language_use_case_1.CheckIssueCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution)); + const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); + return new issue_comment_use_case_1.IssueCommentUseCase(new check_issue_comment_language_use_case_1.CheckIssueCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution), pullRequestDescription); } function createPullRequestReviewCommentUseCaseCompositionRoot() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); @@ -74988,21 +75382,34 @@ function createPullRequestReviewCommentUseCaseCompositionRoot() { const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); - return new pull_request_review_comment_use_case_1.PullRequestReviewCommentUseCase(new check_pull_request_comment_language_use_case_1.CheckPullRequestCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution)); + const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); + return new pull_request_review_comment_use_case_1.PullRequestReviewCommentUseCase(new check_pull_request_comment_language_use_case_1.CheckPullRequestCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution), pullRequestDescription); } function createCommitUseCaseCompositionRoot(projectBoardCommandPort) { return new commit_use_case_1.CommitUseCase(new notify_new_commit_on_issue_use_case_1.NotifyNewCommitOnIssueUseCase((0, issue_interaction_composition_root_1.createIssueNotificationRepository)()), new check_changes_issue_size_use_case_1.CheckChangesIssueSizeUseCase(projectBoardCommandPort, (0, issue_labels_composition_root_1.createIssueLabelRepository)(), new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)())), createDetectPotentialProblemsUseCase(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)()); } function createMainRunRouteCompositionRoot(projectBoardCommandPort) { + // Composition is scoped to one main run. Each route is built only when it is + // actually selected, while repeated calls in the same run reuse its graph. + const singleAction = lazy(() => createSingleActionUseCaseCompositionRoot()); + const issueComment = lazy(() => createIssueCommentUseCaseCompositionRoot()); + const issue = lazy(() => (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)()); + const pullRequestReviewComment = lazy(() => createPullRequestReviewCommentUseCaseCompositionRoot()); + const pullRequest = lazy(() => (0, pull_request_use_case_composition_root_1.createPullRequestUseCaseCompositionRoot)()); + const push = lazy(() => createCommitUseCaseCompositionRoot(projectBoardCommandPort)); return { - "single-action": async (execution) => createSingleActionUseCaseCompositionRoot().invoke(execution), - "issue-comment": async (execution) => createIssueCommentUseCaseCompositionRoot().invoke(execution), - issue: async (execution) => (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)().invoke(execution), - "pull-request-review-comment": async (execution) => createPullRequestReviewCommentUseCaseCompositionRoot().invoke(execution), - "pull-request": async (execution) => (0, pull_request_use_case_composition_root_1.createPullRequestUseCaseCompositionRoot)().invoke(execution), - push: async (execution) => createCommitUseCaseCompositionRoot(projectBoardCommandPort).invoke(execution), + "single-action": async (execution) => singleAction().invoke(execution), + "issue-comment": async (execution) => issueComment().invoke(execution), + issue: async (execution) => issue().invoke(execution), + "pull-request-review-comment": async (execution) => pullRequestReviewComment().invoke(execution), + "pull-request": async (execution) => pullRequest().invoke(execution), + push: async (execution) => push().invoke(execution), }; } +function lazy(factory) { + let value; + return () => value ?? (value = factory()); +} /***/ }), @@ -76306,15 +76713,17 @@ exports.ConfigurationHandler = ConfigurationHandler; /***/ }), /***/ 58043: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildConfigurationPayload = buildConfigurationPayload; +const config_1 = __nccwpck_require__(90450); function buildConfigurationPayload(execution, storedRaw) { const current = execution.currentConfiguration; const payload = { + schemaVersion: config_1.CONFIG_SCHEMA_VERSION, branchType: current.branchType, releaseBranch: current.releaseBranch, workingBranch: current.workingBranch, @@ -77196,6 +77605,7 @@ exports.INPUT_KEYS = { RELEASE_COMMAND: 'release-command', // AI configuration AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', + AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', AI_MEMBERS_ONLY: 'ai-members-only', AI_IGNORE_FILES: 'ai-ignore-files', AI_INCLUDE_REASONING: 'ai-include-reasoning', diff --git a/build/cli/src/actions/github_action_ai_inputs.d.ts b/build/cli/src/actions/github_action_ai_inputs.d.ts index a1a5d779..0a527f9e 100644 --- a/build/cli/src/actions/github_action_ai_inputs.d.ts +++ b/build/cli/src/actions/github_action_ai_inputs.d.ts @@ -1,7 +1,9 @@ import type { AgentTaskConfiguration } from '../data/model/agent'; +import { type PullRequestDescriptionMode } from '../domain/pull_request_description'; export interface GithubActionAiInputs { readonly requestedAgentTasks: AgentTaskConfiguration; readonly pullRequestDescription: boolean; + readonly pullRequestDescriptionMode: PullRequestDescriptionMode; readonly membersOnly: boolean; readonly includeReasoning: boolean; readonly ignoreFiles: string[]; diff --git a/build/cli/src/actions/local_action_configuration.d.ts b/build/cli/src/actions/local_action_configuration.d.ts index 748e9222..f6ab15af 100644 --- a/build/cli/src/actions/local_action_configuration.d.ts +++ b/build/cli/src/actions/local_action_configuration.d.ts @@ -115,6 +115,7 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn agentTasks: import("../domain/agent").AgentTaskConfiguration; agentModel: string; aiPullRequestDescription: boolean; + aiPullRequestDescriptionMode: "replace" | "append" | "preserve" | "disabled"; aiMembersOnly: boolean; aiIncludeReasoning: boolean; aiIgnoreFilesInput: string; diff --git a/build/cli/src/actions/local_action_configuration_sections.d.ts b/build/cli/src/actions/local_action_configuration_sections.d.ts index 740ed554..7b0d3a20 100644 --- a/build/cli/src/actions/local_action_configuration_sections.d.ts +++ b/build/cli/src/actions/local_action_configuration_sections.d.ts @@ -18,6 +18,7 @@ export declare function readLocalAgentConfiguration(additionalParams: ActionInpu agentTasks: import("../domain/agent").AgentTaskConfiguration; agentModel: string; aiPullRequestDescription: boolean; + aiPullRequestDescriptionMode: "replace" | "append" | "preserve" | "disabled"; aiMembersOnly: boolean; aiIncludeReasoning: boolean; aiIgnoreFilesInput: string; diff --git a/build/cli/src/application/errors/application_error.d.ts b/build/cli/src/application/errors/application_error.d.ts new file mode 100644 index 00000000..69ed4cca --- /dev/null +++ b/build/cli/src/application/errors/application_error.d.ts @@ -0,0 +1,13 @@ +export type ApplicationErrorKind = 'configuration' | 'authorization' | 'provider' | 'agent' | 'validation' | 'workflow' | 'unknown'; +export interface ApplicationErrorOptions { + readonly retryable?: boolean; + readonly cause?: unknown; +} +/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ +export declare class ApplicationError extends Error { + readonly kind: ApplicationErrorKind; + readonly retryable: boolean; + readonly cause?: unknown; + constructor(message: string, kind?: ApplicationErrorKind, options?: ApplicationErrorOptions); +} +export declare function toApplicationError(error: unknown, message: string, kind?: ApplicationErrorKind, options?: ApplicationErrorOptions): ApplicationError; diff --git a/build/cli/src/application/policies/action_summary_policy.d.ts b/build/cli/src/application/policies/action_summary_policy.d.ts index aeb86812..edd3e792 100644 --- a/build/cli/src/application/policies/action_summary_policy.d.ts +++ b/build/cli/src/application/policies/action_summary_policy.d.ts @@ -6,6 +6,7 @@ export interface ActionSummaryContext { readonly issueNumber: number; readonly pullRequestNumber: number; readonly lifecycleState?: string; + readonly pullRequestDescriptionMode?: string; readonly results: readonly Result[]; } /** Builds a bounded, publication-safe GitHub Actions Job Summary. */ diff --git a/build/cli/src/application/policies/lifecycle_state_policy.d.ts b/build/cli/src/application/policies/lifecycle_state_policy.d.ts index 8f63269a..e8c94567 100644 --- a/build/cli/src/application/policies/lifecycle_state_policy.d.ts +++ b/build/cli/src/application/policies/lifecycle_state_policy.d.ts @@ -1,4 +1,11 @@ import type { CopilotLifecycleState } from '../../domain/copilot_lifecycle'; +import type { ExecutionInputs } from '../../data/model/execution_inputs'; +export type LifecycleChecksEvidence = 'pending' | 'success' | 'failure'; +export type LifecycleReviewEvidence = 'approved' | 'changes-requested' | 'commented' | 'dismissed'; +export interface LifecycleExternalEvidence { + readonly checks?: LifecycleChecksEvidence; + readonly review?: LifecycleReviewEvidence; +} export interface LifecycleStatePolicyResult { readonly id: string; readonly success: boolean; @@ -16,7 +23,10 @@ export interface LifecycleStateDecisionInput { readonly issueDescriptionEdited: boolean; readonly pullRequestMerged: boolean; readonly pullRequestClosed: boolean; + readonly externalEvidence?: LifecycleExternalEvidence; readonly results: readonly LifecycleStatePolicyResult[]; } /** Resolves the next lifecycle state from application facts, never from labels or API responses. */ export declare function resolveLifecycleState(input: LifecycleStateDecisionInput): CopilotLifecycleState | undefined; +/** Extracts only stable review/check facts from GitHub event payloads. */ +export declare function readLifecycleExternalEvidence(inputs: ExecutionInputs | undefined): LifecycleExternalEvidence | undefined; diff --git a/build/cli/src/application/policies/status_command_policy.d.ts b/build/cli/src/application/policies/status_command_policy.d.ts new file mode 100644 index 00000000..b6ae9682 --- /dev/null +++ b/build/cli/src/application/policies/status_command_policy.d.ts @@ -0,0 +1,26 @@ +import type { Execution } from '../../data/model/execution'; +import { Result } from '../../data/model/result'; +export interface CopilotStatusSnapshot { + readonly owner: string; + readonly repository: string; + readonly event: string; + readonly action: string; + readonly target: 'issue' | 'pull-request' | 'push' | 'repository'; + readonly issueNumber?: number; + readonly pullRequestNumber?: number; + readonly branch?: string; + readonly lifecycle?: string; + readonly waitingFor?: string; + readonly issueLabels: readonly string[]; + readonly pullRequestLabels: readonly string[]; + readonly activeFindings?: { + open: number; + reopened: number; + resolved: number; + }; + readonly pullRequestDescriptionMode: string; +} +/** Builds a read-only status snapshot from the facts already loaded by setup. */ +export declare function buildCopilotStatusSnapshot(execution: Execution): CopilotStatusSnapshot; +export declare function buildCopilotStatusResult(execution: Execution, taskId: string): Result; +export declare function formatCopilotStatus(snapshot: CopilotStatusSnapshot): string; diff --git a/build/cli/src/application/ports/execution_resolution_ports.d.ts b/build/cli/src/application/ports/execution_resolution_ports.d.ts index 9452fb7e..d0a300cf 100644 --- a/build/cli/src/application/ports/execution_resolution_ports.d.ts +++ b/build/cli/src/application/ports/execution_resolution_ports.d.ts @@ -17,6 +17,7 @@ export interface ExecutionIssueResolutionContext { }; pullRequest: { head: string; + number?: number; }; commit: { branch: string; diff --git a/build/cli/src/application/ports/pull_request_description_ports.d.ts b/build/cli/src/application/ports/pull_request_description_ports.d.ts index 8b675524..dfa4768f 100644 --- a/build/cli/src/application/ports/pull_request_description_ports.d.ts +++ b/build/cli/src/application/ports/pull_request_description_ports.d.ts @@ -1,3 +1,10 @@ +export interface PullRequestDescriptionDetails { + readonly body: string; + readonly headBranch: string; + readonly baseBranch: string; +} export interface PullRequestDescriptionCommandPort { updateDescription(owner: string, repository: string, pullRequestNumber: number, description: string, token: string): Promise; + /** Optional read capability used by explicit commands from issue comments. */ + getDetails?(owner: string, repository: string, pullRequestNumber: number, token: string): Promise; } diff --git a/build/cli/src/application/usecases/comment_automation_contracts.d.ts b/build/cli/src/application/usecases/comment_automation_contracts.d.ts index 9e9a75b1..de79d5a8 100644 --- a/build/cli/src/application/usecases/comment_automation_contracts.d.ts +++ b/build/cli/src/application/usecases/comment_automation_contracts.d.ts @@ -5,6 +5,9 @@ import type { BugbotAutofixParam } from "./steps/commit/bugbot/bugbot_autofix_us import type { DoUserRequestParam } from "./steps/commit/user_request_use_case"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +export interface ExplicitPullRequestDescriptionUseCase { + invokeExplicit(param: Execution): Promise; +} export interface CommentAutomationOptions { taskId: string; languageUseCase: ParamUseCase; @@ -17,4 +20,6 @@ export interface CommentAutomationOptions { userComment: string; gitCommitPort: GitCommitPort; dismissBugbotFindingsUseCase?: ParamUseCase; + /** Optional explicit PR description command; automatic PR updates remain a separate route. */ + updatePullRequestDescriptionUseCase?: ExplicitPullRequestDescriptionUseCase; } diff --git a/build/cli/src/application/usecases/issue_comment_use_case.d.ts b/build/cli/src/application/usecases/issue_comment_use_case.d.ts index b5dddcc6..04efabe4 100644 --- a/build/cli/src/application/usecases/issue_comment_use_case.d.ts +++ b/build/cli/src/application/usecases/issue_comment_use_case.d.ts @@ -9,6 +9,7 @@ import type { ActorAuthorizationPort } from "../ports/actor_authorization_ports" import type { BugbotFindingResolutionPorts } from "../ports/bugbot_finding_resolution_ports"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +import type { UpdatePullRequestDescriptionUseCase } from './steps/pull_request/update_pull_request_description_use_case'; export declare class IssueCommentUseCase implements ParamUseCase { private readonly languageUseCase; private readonly intentUseCase; @@ -22,7 +23,8 @@ export declare class IssueCommentUseCase implements ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined); + constructor(languageUseCase: ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined, updatePullRequestDescriptionUseCase?: UpdatePullRequestDescriptionUseCase | undefined); invoke(param: Execution): Promise; } diff --git a/build/cli/src/application/usecases/pull_request_review_comment_use_case.d.ts b/build/cli/src/application/usecases/pull_request_review_comment_use_case.d.ts index ad194518..e7cba57e 100644 --- a/build/cli/src/application/usecases/pull_request_review_comment_use_case.d.ts +++ b/build/cli/src/application/usecases/pull_request_review_comment_use_case.d.ts @@ -9,6 +9,7 @@ import type { ActorAuthorizationPort } from "../ports/actor_authorization_ports" import type { BugbotFindingResolutionPorts } from "../ports/bugbot_finding_resolution_ports"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +import type { UpdatePullRequestDescriptionUseCase } from './steps/pull_request/update_pull_request_description_use_case'; export declare class PullRequestReviewCommentUseCase implements ParamUseCase { private readonly languageUseCase; private readonly intentUseCase; @@ -22,7 +23,8 @@ export declare class PullRequestReviewCommentUseCase implements ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined); + constructor(languageUseCase: ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined, updatePullRequestDescriptionUseCase?: UpdatePullRequestDescriptionUseCase | undefined); invoke(param: Execution): Promise; } diff --git a/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts b/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts index 738c1f94..bd583633 100644 --- a/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts +++ b/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts @@ -14,4 +14,6 @@ export declare class UpdatePullRequestDescriptionUseCase implements ParamUseCase taskId: string; constructor(pullRequestDescriptionCommandPort: PullRequestDescriptionCommandPort, issueDescriptionQueryPort: IssueDescriptionQueryPort, organizationMembersPort: OrganizationMembersPort, aiRepository: FindingsQueryPort); invoke(param: Execution): Promise; + /** Explicit comment commands may update a preserved PR body on demand. */ + invokeExplicit(param: Execution): Promise; } diff --git a/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts b/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts index 0d5633da..57c83a9f 100644 --- a/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts +++ b/build/cli/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts @@ -11,4 +11,4 @@ export interface UpdatePullRequestDescriptionWorkflowDependencies { aiRepository: FindingsQueryPort; } /** Generates and publishes a PR description while keeping provider details behind ports. */ -export declare function runUpdatePullRequestDescriptionWorkflow(param: Execution, taskId: string, dependencies: UpdatePullRequestDescriptionWorkflowDependencies): Promise; +export declare function runUpdatePullRequestDescriptionWorkflow(param: Execution, taskId: string, dependencies: UpdatePullRequestDescriptionWorkflowDependencies, force?: boolean): Promise; diff --git a/build/cli/src/cli/commands/reconcile.d.ts b/build/cli/src/cli/commands/reconcile.d.ts new file mode 100644 index 00000000..b9a89bbc --- /dev/null +++ b/build/cli/src/cli/commands/reconcile.d.ts @@ -0,0 +1,10 @@ +import { Command } from 'commander'; +import type { SetupWorkspacePort } from '../../application/ports/setup_workspace_ports'; +export interface ReconcileCommandOptions { + config?: string; + apply?: boolean; + json?: boolean; +} +/** Reconciles setup-managed workflow files locally; remote GitHub state is never changed. */ +export declare function registerReconcileCommand(program: Command): void; +export declare function runReconcileCommand(options: ReconcileCommandOptions, workspace?: SetupWorkspacePort): void; diff --git a/build/cli/src/data/model/ai.d.ts b/build/cli/src/data/model/ai.d.ts index 68ae90c9..759c99a2 100644 --- a/build/cli/src/data/model/ai.d.ts +++ b/build/cli/src/data/model/ai.d.ts @@ -1,4 +1,5 @@ import { AgentConfiguration, AgentTask, AgentTaskConfiguration } from './agent'; +import { type PullRequestDescriptionMode } from '../../domain/pull_request_description'; export declare class Ai { private aiPullRequestDescription; private aiMembersOnly; @@ -8,8 +9,10 @@ export declare class Ai { private bugbotCommentLimit; private bugbotFixVerifyCommands; private agentTasks; - constructor(_configurationSource: string, model: string, aiPullRequestDescription: boolean, aiMembersOnly: boolean, aiIgnoreFiles: string[], aiIncludeReasoning: boolean, bugbotMinSeverity: string, bugbotCommentLimit: number, bugbotFixVerifyCommands?: string[], agentTasks?: AgentTaskConfiguration); + private pullRequestDescriptionMode; + constructor(_configurationSource: string, model: string, aiPullRequestDescription: boolean, aiMembersOnly: boolean, aiIgnoreFiles: string[], aiIncludeReasoning: boolean, bugbotMinSeverity: string, bugbotCommentLimit: number, bugbotFixVerifyCommands?: string[], agentTasks?: AgentTaskConfiguration, pullRequestDescriptionMode?: PullRequestDescriptionMode); getAiPullRequestDescription(): boolean; + getPullRequestDescriptionMode(): PullRequestDescriptionMode; getAiMembersOnly(): boolean; getAiIgnoreFiles(): string[]; getAiIncludeReasoning(): boolean; diff --git a/build/cli/src/data/model/config.d.ts b/build/cli/src/data/model/config.d.ts index bdab7e9f..3efd7156 100644 --- a/build/cli/src/data/model/config.d.ts +++ b/build/cli/src/data/model/config.d.ts @@ -1,7 +1,9 @@ import { BranchConfiguration } from "./branch_configuration"; import { RecommendationState } from "./recommendation_state"; import { Result } from "./result"; +export declare const CONFIG_SCHEMA_VERSION = 1; export declare class Config { + readonly schemaVersion: number; branchType: string; releaseBranch: string | undefined; workingBranch: string | undefined; diff --git a/build/cli/src/data/model/execution_inputs.d.ts b/build/cli/src/data/model/execution_inputs.d.ts index 899e3b71..b61005ab 100644 --- a/build/cli/src/data/model/execution_inputs.d.ts +++ b/build/cli/src/data/model/execution_inputs.d.ts @@ -35,6 +35,25 @@ export interface EventPullRequestPayload { merged?: boolean; state?: string; } +export interface EventPullRequestReferencePayload { + number?: number; +} +export interface EventReviewPayload { + state?: string; + pull_request?: EventPullRequestReferencePayload; +} +export interface EventCheckSuitePayload { + status?: string; + conclusion?: string | null; + head_branch?: string; + pull_requests?: EventPullRequestReferencePayload[]; +} +export interface EventWorkflowRunPayload { + status?: string; + conclusion?: string | null; + head_branch?: string; + pull_requests?: EventPullRequestReferencePayload[]; +} export interface EventCommitPayload { id?: string; message?: string; @@ -55,6 +74,9 @@ export interface ExecutionInputs { issue?: EventIssuePayload; label?: EventLabelPayload; pull_request?: EventPullRequestPayload; + review?: EventReviewPayload; + check_suite?: EventCheckSuitePayload; + workflow_run?: EventWorkflowRunPayload; comment?: EventCommentPayload; pull_request_review_comment?: EventCommentPayload; changes?: Record; diff --git a/build/cli/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts b/build/cli/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts index 1e4985f5..033b3bce 100644 --- a/build/cli/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts +++ b/build/cli/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts @@ -1,3 +1,4 @@ +import type { PullRequestDescriptionDetails } from '../../../application/ports/pull_request_description_ports'; import type { GithubClientPort } from "../../../infrastructure/github/ports/github_client_provider_port"; import type { GithubPullRequestLifecycleClient } from "../../../infrastructure/github/ports/github_pull_request_provider_ports"; export declare class PullRequestLifecycleRepository { @@ -21,4 +22,5 @@ export declare class PullRequestLifecycleRepository { isLinked: (pullRequestUrl: string) => Promise; updateBaseBranch: (owner: string, repository: string, pullRequestNumber: number, branch: string, token: string) => Promise; updateDescription: (owner: string, repository: string, pullRequestNumber: number, description: string, token: string) => Promise; + getDetails: (owner: string, repository: string, pullRequestNumber: number, token: string) => Promise; } diff --git a/build/cli/src/domain/copilot_command.d.ts b/build/cli/src/domain/copilot_command.d.ts index 25644aeb..6487f6f4 100644 --- a/build/cli/src/domain/copilot_command.d.ts +++ b/build/cli/src/domain/copilot_command.d.ts @@ -1,5 +1,5 @@ /** Explicit commands are the safe, deterministic entry point for mutations. */ -export declare const COPILOT_COMMAND_NAMES: readonly ["plan", "clarify", "estimate", "test-plan", "status", "review", "findings", "fix", "dismiss", "recheck"]; +export declare const COPILOT_COMMAND_NAMES: readonly ["plan", "clarify", "estimate", "test-plan", "status", "description", "review", "findings", "fix", "dismiss", "recheck"]; export type CopilotCommandName = typeof COPILOT_COMMAND_NAMES[number]; export interface ParsedCopilotCommand { readonly name: CopilotCommandName; diff --git a/build/cli/src/domain/pull_request_description.d.ts b/build/cli/src/domain/pull_request_description.d.ts new file mode 100644 index 00000000..3294625e --- /dev/null +++ b/build/cli/src/domain/pull_request_description.d.ts @@ -0,0 +1,13 @@ +export declare const PULL_REQUEST_DESCRIPTION_MODES: readonly ["replace", "append", "preserve", "disabled"]; +export type PullRequestDescriptionMode = typeof PULL_REQUEST_DESCRIPTION_MODES[number]; +export declare const DEFAULT_PULL_REQUEST_DESCRIPTION_MODE: PullRequestDescriptionMode; +export declare const MANAGED_PULL_REQUEST_DESCRIPTION_START = ""; +export declare const MANAGED_PULL_REQUEST_DESCRIPTION_END = ""; +/** Normalizes public configuration while keeping invalid values safe and backwards compatible. */ +export declare function normalizePullRequestDescriptionMode(value: unknown): PullRequestDescriptionMode; +export declare function hasManagedPullRequestDescription(body: unknown): boolean; +/** Renders one bounded Copilot-owned section without taking ownership of the rest of the body. */ +export declare function renderManagedPullRequestDescription(generated: string): string; +/** Replaces the existing managed section, or appends one when none exists. */ +export declare function mergeManagedPullRequestDescription(currentBody: unknown, generated: string): string; +export declare function shouldAutomaticallyUpdatePullRequestDescription(mode: PullRequestDescriptionMode): boolean; diff --git a/build/cli/src/domain/setup.d.ts b/build/cli/src/domain/setup.d.ts index e02a2dcf..668d18ad 100644 --- a/build/cli/src/domain/setup.d.ts +++ b/build/cli/src/domain/setup.d.ts @@ -1,4 +1,5 @@ import type { AgentProvider, AgentTask } from './agent'; +import type { PullRequestDescriptionMode } from './pull_request_description'; export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'issueTemplates' | 'pullRequestTemplate'; export interface SetupFeatures { [feature: string]: boolean; @@ -30,6 +31,8 @@ export interface SetupRepositoryConfiguration { } export interface SetupAiConfiguration { pullRequestDescription: boolean; + /** Optional for backwards-compatible setup files created before v3.3.0. */ + pullRequestDescriptionMode?: PullRequestDescriptionMode; ignoreFiles: string; membersOnly: boolean; includeReasoning: boolean; diff --git a/build/cli/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts b/build/cli/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts index 5c79941c..a82e2d16 100644 --- a/build/cli/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts +++ b/build/cli/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts @@ -35,6 +35,17 @@ export interface GithubPullRequestLifecycleClient { data: GithubPullRequestSummary[]; }>; update(parameters: Record): Promise; + get?(parameters: Record): Promise<{ + data: { + body?: string | null; + head?: { + ref?: string | null; + }; + base?: { + ref?: string | null; + }; + }; + }>; }; }; } diff --git a/build/cli/src/utils/constants.d.ts b/build/cli/src/utils/constants.d.ts index 43f98677..14f23fdc 100644 --- a/build/cli/src/utils/constants.d.ts +++ b/build/cli/src/utils/constants.d.ts @@ -90,6 +90,7 @@ export declare const INPUT_KEYS: { readonly RELEASE_MODEL: "release-model"; readonly RELEASE_COMMAND: "release-command"; readonly AI_PULL_REQUEST_DESCRIPTION: "ai-pull-request-description"; + readonly AI_PULL_REQUEST_DESCRIPTION_MODE: "ai-pull-request-description-mode"; readonly AI_MEMBERS_ONLY: "ai-members-only"; readonly AI_IGNORE_FILES: "ai-ignore-files"; readonly AI_INCLUDE_REASONING: "ai-include-reasoning"; diff --git a/build/github_action/index.js b/build/github_action/index.js index 140147f5..0415e74b 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -50752,18 +50752,23 @@ const input_boolean_policy_1 = __nccwpck_require__(18330); const input_number_policy_1 = __nccwpck_require__(47165); const input_values_policy_1 = __nccwpck_require__(68841); const agent_input_builder_1 = __nccwpck_require__(71404); +const pull_request_description_1 = __nccwpck_require__(45315); function readGithubActionAgentTasks(getInput, _configurationSource) { return (0, agent_input_builder_1.buildAgentTasksFromInputs)(getInput); } function readGithubActionAiInputs(getInput) { const requestedAgentTasks = (0, agent_input_builder_1.buildAgentTasksFromInputs)(getInput); + const pullRequestDescription = (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); const verifyCommands = getInput(constants_1.INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) .split(',') .map((command) => command.trim()) .filter((command) => command.length > 0); return { requestedAgentTasks, - pullRequestDescription: (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)), + pullRequestDescription, + pullRequestDescriptionMode: pullRequestDescription + ? (0, pull_request_description_1.normalizePullRequestDescriptionMode)(getInput(constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) + : 'disabled', membersOnly: (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.AI_MEMBERS_ONLY)), includeReasoning: (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.AI_INCLUDE_REASONING)), ignoreFiles: (0, input_values_policy_1.parseDelimitedValues)(getInput(constants_1.INPUT_KEYS.AI_IGNORE_FILES)), @@ -50883,6 +50888,7 @@ async function writeActionSummary(execution, summaryPort) { lifecycleState: (0, copilot_lifecycle_1.lifecycleStateFromLabels)(execution.isPullRequest ? execution.labels?.currentPullRequestLabels ?? [] : execution.labels?.currentIssueLabels ?? [], execution.labels?.lifecycle), + pullRequestDescriptionMode: execution.ai?.getPullRequestDescriptionMode?.(), results: execution.currentConfiguration.results, }); if (!summaryPort) @@ -50992,7 +50998,7 @@ async function buildGithubActionExecution(input) { emoji: (0, configuration_builders_1.buildEmoji)(getInput(constants_1.INPUT_KEYS.EMOJI_LABELED_TITLE) === 'true', getInput(constants_1.INPUT_KEYS.BRANCH_MANAGEMENT_EMOJI)), images: (0, configuration_builders_1.buildImages)(imageConfiguration), tokens: (0, configuration_builders_1.buildTokens)(token), - ai: new ai_1.Ai('', aiInputs.requestedAgentTasks.findings.model, aiInputs.pullRequestDescription, aiInputs.membersOnly, aiInputs.ignoreFiles, aiInputs.includeReasoning, aiInputs.bugbotSeverity, aiInputs.bugbotCommentLimit, aiInputs.bugbotFixVerifyCommands, aiInputs.requestedAgentTasks), + ai: new ai_1.Ai('', aiInputs.requestedAgentTasks.findings.model, aiInputs.pullRequestDescription, aiInputs.membersOnly, aiInputs.ignoreFiles, aiInputs.includeReasoning, aiInputs.bugbotSeverity, aiInputs.bugbotCommentLimit, aiInputs.bugbotFixVerifyCommands, aiInputs.requestedAgentTasks, aiInputs.pullRequestDescriptionMode), labels: (0, configuration_builders_1.buildLabels)(labelInputs), issueTypes: (0, configuration_builders_1.buildIssueTypes)(issueTypeInputs), locale: (0, configuration_builders_1.buildLocale)(localeInputs.issue, localeInputs.pullRequest), @@ -51789,6 +51795,34 @@ function resolveWorkflowIdentifier(workflowRef) { } +/***/ }), + +/***/ 75999: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ApplicationError = void 0; +exports.toApplicationError = toApplicationError; +/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ +class ApplicationError extends Error { + constructor(message, kind = 'unknown', options = {}) { + super(message); + this.name = 'ApplicationError'; + this.kind = kind; + this.retryable = options.retryable ?? false; + this.cause = options.cause; + } +} +exports.ApplicationError = ApplicationError; +function toApplicationError(error, message, kind = 'unknown', options = {}) { + return error instanceof ApplicationError + ? error + : new ApplicationError(message, kind, { ...options, cause: error }); +} + + /***/ }), /***/ 72995: @@ -51819,6 +51853,7 @@ function buildActionSummary(context) { `| Event | \`${escapeTable(context.eventName)}\` |`, `| Target | ${escapeTable(target)} |`, `| Lifecycle | ${lifecycle} |`, + `| PR description policy | ${escapeTable(context.pullRequestDescriptionMode ?? '—')} |`, `| Results | ${context.results.length} |`, `| Finding states | ${formatFindingStates(findingStates)} |`, ]; @@ -51944,7 +51979,7 @@ function hasComment(execution) { return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; } function hasTarget(execution) { - if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + if (['pull_request', 'pull_request_review', 'pull_request_review_comment', 'check_suite', 'workflow_run'].includes(execution.eventName)) { return execution.pullRequest.number > 0; } return execution.issue.number > 0 || execution.issueNumber > 0; @@ -52891,6 +52926,7 @@ function buildInitialLabelProvisioningPlan(labels, existingLabelNames) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveLifecycleState = resolveLifecycleState; +exports.readLifecycleExternalEvidence = readLifecycleExternalEvidence; const result_1 = __nccwpck_require__(73817); /** Resolves the next lifecycle state from application facts, never from labels or API responses. */ function resolveLifecycleState(input) { @@ -52901,6 +52937,10 @@ function resolveLifecycleState(input) { if (input.isPullRequest) { if (input.pullRequestClosed && input.pullRequestMerged) return 'verified'; + if (input.externalEvidence?.checks === 'failure') + return 'blocked'; + if (input.externalEvidence?.review === 'changes-requested') + return 'changes-requested'; const findingState = input.results .map(result => (0, result_1.getResultPayload)(result.payload)?.findingStates) .find(isFindingStateCounts); @@ -52908,6 +52948,14 @@ function resolveLifecycleState(input) { return 'changes-requested'; if (findingState && findingState.open === 0 && findingState.reopened === 0) return 'ready'; + if (input.externalEvidence?.checks === 'pending') + return 'reviewing'; + if (input.externalEvidence?.review === 'approved') + return 'ready'; + if (input.externalEvidence?.checks === 'success') + return 'reviewing'; + if (input.externalEvidence?.review !== undefined) + return 'reviewing'; if (['opened', 'reopened', 'synchronize'].includes(input.action)) return 'reviewing'; return undefined; @@ -52920,6 +52968,35 @@ function resolveLifecycleState(input) { return 'planned'; return undefined; } +/** Extracts only stable review/check facts from GitHub event payloads. */ +function readLifecycleExternalEvidence(inputs) { + if (!inputs) + return undefined; + if (inputs.eventName === 'pull_request_review') { + const reviewState = inputs.review?.state?.trim().toLowerCase(); + if (reviewState === 'approved') + return { review: 'approved' }; + if (reviewState === 'changes_requested') + return { review: 'changes-requested' }; + if (reviewState === 'dismissed') + return { review: 'dismissed' }; + if (reviewState === 'commented') + return { review: 'commented' }; + return undefined; + } + if (inputs.eventName === 'check_suite') { + return { checks: readChecksEvidence(inputs.check_suite?.status, inputs.check_suite?.conclusion) }; + } + if (inputs.eventName === 'workflow_run') { + return { checks: readChecksEvidence(inputs.workflow_run?.status, inputs.workflow_run?.conclusion) }; + } + return undefined; +} +function readChecksEvidence(status, conclusion) { + if (status?.trim().toLowerCase() !== 'completed') + return 'pending'; + return conclusion?.trim().toLowerCase() === 'success' ? 'success' : 'failure'; +} function isFindingStateCounts(value) { return typeof value === 'object' && value !== null @@ -52978,7 +53055,10 @@ function isHumanInteraction(eventName) { 'issues', 'issue_comment', 'pull_request', + 'pull_request_review', 'pull_request_review_comment', + 'check_suite', + 'workflow_run', 'push', ].includes(eventName); } @@ -53361,6 +53441,7 @@ exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; exports.buildSetupActionInputs = buildSetupActionInputs; const agent_1 = __nccwpck_require__(89040); const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); +const pull_request_description_1 = __nccwpck_require__(45315); exports.SETUP_AGENT_TASKS = [ 'planner', 'findings', @@ -53441,6 +53522,7 @@ function createDefaultSetupConfiguration() { }, ai: { pullRequestDescription: true, + pullRequestDescriptionMode: 'replace', ignoreFiles: 'build/*', membersOnly: false, includeReasoning: true, @@ -53510,6 +53592,10 @@ function validateSetupConfiguration(configuration) { if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { errors.push('Bugbot severity must be info, low, medium, or high.'); } + if (configuration.ai.pullRequestDescriptionMode !== undefined + && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { + errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); + } if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } @@ -53615,6 +53701,7 @@ function buildSetupRepositoryVariables(configuration) { add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); + add('AI_PULL_REQUEST_DESCRIPTION_MODE', configuration.ai.pullRequestDescriptionMode); add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); @@ -53650,6 +53737,7 @@ function buildSetupActionInputs(configuration) { 'pull-requests-locale': repository.pullRequestLocale, 'commit-prefix-transforms': repository.commitPrefixTransforms, 'ai-pull-request-description': String(ai.pullRequestDescription), + 'ai-pull-request-description-mode': (0, pull_request_description_1.normalizePullRequestDescriptionMode)(ai.pullRequestDescriptionMode), 'ai-ignore-files': ai.ignoreFiles, 'ai-members-only': String(ai.membersOnly), 'ai-include-reasoning': String(ai.includeReasoning), @@ -53708,6 +53796,103 @@ function unique(values) { } +/***/ }), + +/***/ 3449: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildCopilotStatusSnapshot = buildCopilotStatusSnapshot; +exports.buildCopilotStatusResult = buildCopilotStatusResult; +exports.formatCopilotStatus = formatCopilotStatus; +const result_1 = __nccwpck_require__(73817); +/** Builds a read-only status snapshot from the facts already loaded by setup. */ +function buildCopilotStatusSnapshot(execution) { + const issueLabels = [...(execution.labels?.currentIssueLabels ?? [])]; + const pullRequestLabels = [...(execution.labels?.currentPullRequestLabels ?? [])]; + const isPullRequestTarget = execution.isPullRequest || execution.pullRequest?.number > 0 || execution.pullRequest?.isPullRequestReviewComment; + const targetLabels = isPullRequestTarget ? pullRequestLabels : issueLabels; + const lifecycleLabels = execution.labels?.lifecycle ?? {}; + const lifecycle = Object.entries({ + planned: lifecycleLabels.planned, + 'in-progress': lifecycleLabels.inProgress, + reviewing: lifecycleLabels.reviewing, + 'changes-requested': lifecycleLabels.changesRequested, + verified: lifecycleLabels.verified, + ready: lifecycleLabels.ready, + blocked: lifecycleLabels.blocked, + }).find(([, label]) => label && targetLabels.includes(label))?.[0]; + const waitingFor = Object.entries({ + maintainer: lifecycleLabels.awaitingMaintainer, + 'issue-author': lifecycleLabels.awaitingIssueAuthor, + }).find(([, label]) => label && targetLabels.includes(label))?.[0]; + const findingStates = execution.currentConfiguration?.results + ?.map(result => (0, result_1.getResultPayload)(result.payload)?.findingStates) + .find(isFindingStateCounts); + return { + owner: execution.owner, + repository: execution.repo, + event: execution.eventName || 'unknown', + action: execution.inputs?.action ?? '', + target: execution.pullRequest?.number > 0 || execution.pullRequest?.isPullRequestReviewComment + ? 'pull-request' + : execution.isPush + ? 'push' + : execution.issue?.number > 0 || execution.isIssue + ? 'issue' + : 'repository', + ...(execution.issue?.number > 0 ? { issueNumber: execution.issue.number } : {}), + ...(execution.pullRequest?.number > 0 ? { pullRequestNumber: execution.pullRequest.number } : {}), + ...(execution.commit?.branch ? { branch: execution.commit.branch } : {}), + ...(lifecycle ? { lifecycle } : {}), + ...(waitingFor ? { waitingFor } : {}), + issueLabels, + pullRequestLabels, + ...(findingStates ? { activeFindings: findingStates } : {}), + pullRequestDescriptionMode: execution.ai.getPullRequestDescriptionMode?.() + ?? (execution.ai.getAiPullRequestDescription() ? 'replace' : 'disabled'), + }; +} +function buildCopilotStatusResult(execution, taskId) { + const snapshot = buildCopilotStatusSnapshot(execution); + return new result_1.Result({ + id: `${taskId}.Status`, + success: true, + executed: true, + stepFormat: 'markdown', + steps: [formatCopilotStatus(snapshot)], + payload: { status: snapshot }, + }); +} +function formatCopilotStatus(snapshot) { + const lines = [ + '## Copilot status', + `- **Repository:** ${snapshot.owner}/${snapshot.repository}`, + `- **Target:** ${snapshot.target}${snapshot.issueNumber ? ` #${snapshot.issueNumber}` : ''}${snapshot.pullRequestNumber ? ` / PR #${snapshot.pullRequestNumber}` : ''}`, + `- **Event:** ${snapshot.event}${snapshot.action ? ` (${snapshot.action})` : ''}`, + `- **Branch:** ${snapshot.branch ?? 'unknown'}`, + `- **Lifecycle:** ${snapshot.lifecycle ?? 'not set'}`, + `- **Waiting for:** ${snapshot.waitingFor ?? 'no pending human response'}`, + `- **PR description policy:** ${snapshot.pullRequestDescriptionMode}`, + `- **Issue labels:** ${snapshot.issueLabels.length > 0 ? snapshot.issueLabels.join(', ') : 'none'}`, + `- **PR labels:** ${snapshot.pullRequestLabels.length > 0 ? snapshot.pullRequestLabels.join(', ') : 'none'}`, + ]; + if (snapshot.activeFindings) { + lines.push(`- **Bugbot findings:** ${snapshot.activeFindings.open} open, ${snapshot.activeFindings.reopened} reopened, ${snapshot.activeFindings.resolved} resolved`); + } + return lines.join('\n'); +} +function isFindingStateCounts(value) { + return typeof value === 'object' + && value !== null + && typeof value.open === 'number' + && typeof value.reopened === 'number' + && typeof value.resolved === 'number'; +} + + /***/ }), /***/ 43193: @@ -55161,6 +55346,13 @@ const copilot_lifecycle_1 = __nccwpck_require__(72418); const lifecycle_state_policy_1 = __nccwpck_require__(34026); const lifecycle_waiting_state_policy_1 = __nccwpck_require__(61736); const logging_ports_1 = __nccwpck_require__(6152); +const PULL_REQUEST_LIFECYCLE_EVENTS = [ + 'pull_request', + 'pull_request_review', + 'pull_request_review_comment', + 'check_suite', + 'workflow_run', +]; /** * Reconciles one state label after a route completes. The existing business * labels remain untouched, and repeated events are idempotent. @@ -55175,11 +55367,12 @@ class SynchronizeLifecycleStateUseCase { eventName: param.execution.eventName, action: param.execution.inputs?.action ?? '', isIssue: ['issues', 'issue_comment'].includes(param.execution.eventName), - isPullRequest: ['pull_request', 'pull_request_review_comment'].includes(param.execution.eventName), + isPullRequest: param.execution.isPullRequest || PULL_REQUEST_LIFECYCLE_EVENTS.includes(param.execution.eventName), issueOpened: param.execution.issue.opened, issueDescriptionEdited: param.execution.issue.descriptionEdited, pullRequestMerged: param.execution.pullRequest.isMerged, pullRequestClosed: param.execution.pullRequest.isClosed, + externalEvidence: (0, lifecycle_state_policy_1.readLifecycleExternalEvidence)(param.execution.inputs), results: param.results, }); const waitingDecision = (0, lifecycle_waiting_state_policy_1.resolveLifecycleWaitingState)({ @@ -55221,17 +55414,17 @@ function targetNumber(execution) { if (['issues', 'issue_comment', 'push'].includes(execution.eventName)) { return execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; } - if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) + if (PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName)) return execution.pullRequest.number; return -1; } function targetLabels(execution) { - return ['pull_request', 'pull_request_review_comment'].includes(execution.eventName) + return PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName) ? execution.labels.currentPullRequestLabels : execution.labels.currentIssueLabels; } function setTargetLabels(execution, labels) { - if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) { + if (PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName)) { execution.labels.currentPullRequestLabels = labels; } else @@ -55334,16 +55527,32 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; const result_1 = __nccwpck_require__(73817); +const status_command_policy_1 = __nccwpck_require__(3449); /** Executes deterministic /copilot commands without routing them through intent detection. */ async function runExplicitCommentCommand(param, options, command, actorAuthorizationPort) { + if (command.name === 'status') + return [(0, status_command_policy_1.buildCopilotStatusResult)(param, options.taskId)]; if (command.name === 'dismiss') return runDismissCommand(param, options, command, actorAuthorizationPort); + if (command.name === 'description') + return runDescriptionCommand(param, options); if (['review', 'findings', 'recheck'].includes(command.name)) return runReviewCommand(param, options, command); if (command.name === 'fix') return undefined; return runThinkCommand(param, options, command); } +async function runDescriptionCommand(param, options) { + if (!options.updatePullRequestDescriptionUseCase) { + return [new result_1.Result({ + id: `${options.taskId}.Description`, + success: false, + executed: false, + errors: ['Explicit pull-request description command is not available in this composition.'], + })]; + } + return options.updatePullRequestDescriptionUseCase.invokeExplicit(param); +} async function runDismissCommand(param, options, command, actorAuthorizationPort) { const allowed = await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.actor, param.tokens.token); if (!allowed || !options.dismissBugbotFindingsUseCase) { @@ -55520,12 +55729,7 @@ const logging_ports_1 = __nccwpck_require__(6152); const copilot_command_1 = __nccwpck_require__(11771); const comment_automation_command_workflow_1 = __nccwpck_require__(63134); const comment_automation_natural_language_workflow_1 = __nccwpck_require__(10554); -class CommentAutomationError extends Error { - constructor() { - super("Comment automation failed."); - this.name = "CommentAutomationError"; - } -} +const application_error_1 = __nccwpck_require__(75999); async function runCommentAutomation(param, options, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts) { (0, logging_ports_1.logInfo)(`${options.taskId} started.`); let languageResults = []; @@ -55545,8 +55749,8 @@ async function runCommentAutomation(param, options, actorAuthorizationPort, auth bugbotResolutionPorts, }); } - catch { - const error = new CommentAutomationError(); + catch (cause) { + const error = new application_error_1.ApplicationError("Comment automation failed.", 'workflow', { cause }); (0, logging_ports_1.logError)(error); return [...languageResults, new result_1.Result({ id: options.taskId, @@ -55699,8 +55903,13 @@ const title_utils_1 = __nccwpck_require__(46267); function resolveEventIssueNumber(execution) { if (execution.isIssue) return positiveIssueNumberOrUndefined(execution.issue.number); - if (execution.isPullRequest) - return positiveIssueNumberOrUndefined((0, title_utils_1.extractIssueNumberFromBranch)(execution.pullRequest.head)); + if (execution.isPullRequest) { + if (['check_suite', 'workflow_run'].includes(String(execution.inputs?.eventName ?? ''))) { + return positiveIssueNumberOrUndefined(execution.pullRequest.number); + } + return positiveIssueNumberOrUndefined((0, title_utils_1.extractIssueNumberFromBranch)(execution.pullRequest.head)) + ?? positiveIssueNumberOrUndefined(execution.pullRequest.number); + } if (execution.isPush) return positiveIssueNumberOrUndefined((0, title_utils_1.extractIssueNumberFromPush)(execution.commit.branch)); return positiveIssueNumberOrUndefined(execution.issueNumber); @@ -55952,7 +56161,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.IssueCommentUseCase = void 0; const comment_automation_use_case_1 = __nccwpck_require__(9661); class IssueCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -55965,6 +56174,7 @@ class IssueCommentUseCase { this.gitCommitPort = gitCommitPort; this.dismissBugbotFindingsUseCase = dismissBugbotFindingsUseCase; this.reviewPotentialProblemsUseCase = reviewPotentialProblemsUseCase; + this.updatePullRequestDescriptionUseCase = updatePullRequestDescriptionUseCase; this.taskId = "IssueCommentUseCase"; } async invoke(param) { @@ -55979,6 +56189,7 @@ class IssueCommentUseCase { gitCommitPort: this.gitCommitPort, dismissBugbotFindingsUseCase: this.dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase: this.reviewPotentialProblemsUseCase, + updatePullRequestDescriptionUseCase: this.updatePullRequestDescriptionUseCase, }, this.actorAuthorizationPort, this.authenticatedUserPort, this.bugbotResolutionPorts); } } @@ -56097,7 +56308,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewCommentUseCase = void 0; const comment_automation_use_case_1 = __nccwpck_require__(9661); class PullRequestReviewCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, issueCommentUpdatePort, actorAuthorizationPort, authenticatedUserPort, bugbotResolutionPorts, gitCommitPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -56110,6 +56321,7 @@ class PullRequestReviewCommentUseCase { this.gitCommitPort = gitCommitPort; this.dismissBugbotFindingsUseCase = dismissBugbotFindingsUseCase; this.reviewPotentialProblemsUseCase = reviewPotentialProblemsUseCase; + this.updatePullRequestDescriptionUseCase = updatePullRequestDescriptionUseCase; this.taskId = "PullRequestReviewCommentUseCase"; } async invoke(param) { @@ -56124,6 +56336,7 @@ class PullRequestReviewCommentUseCase { gitCommitPort: this.gitCommitPort, dismissBugbotFindingsUseCase: this.dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase: this.reviewPotentialProblemsUseCase, + updatePullRequestDescriptionUseCase: this.updatePullRequestDescriptionUseCase, }, this.actorAuthorizationPort, this.authenticatedUserPort, this.bugbotResolutionPorts); } } @@ -56172,6 +56385,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runPullRequestWorkflow = runPullRequestWorkflow; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); +const application_error_1 = __nccwpck_require__(75999); /** Coordinates pull-request lifecycle actions while preserving their sequential order. */ async function runPullRequestWorkflow(param, taskId, ports) { try { @@ -56187,14 +56401,14 @@ async function runPullRequestWorkflow(param, taskId, ports) { ports.workflowSteps.checkPriorityPullRequestSize, ]; const results = await runSteps(param, steps); - if (param.ai.getAiPullRequestDescription()) { + if (shouldUpdatePullRequestDescriptionAutomatically(param)) { results.push(...(await ports.updatePullRequestDescriptionUseCase.invoke(param))); } results.push(...(await runPullRequestReview(param, ports))); return results; } if (param.pullRequest.isSynchronize) { - const results = param.ai.getAiPullRequestDescription() + const results = shouldUpdatePullRequestDescriptionAutomatically(param) ? await ports.updatePullRequestDescriptionUseCase.invoke(param) : []; results.push(...(await runPullRequestReview(param, ports))); @@ -56204,8 +56418,8 @@ async function runPullRequestWorkflow(param, taskId, ports) { return ports.workflowSteps.closeIssueAfterMerging.invoke(param); } } - catch { - const semanticError = new Error("Unable to process the pull request."); + catch (cause) { + const semanticError = new application_error_1.ApplicationError("Unable to process the pull request.", 'workflow', { cause }); (0, logging_ports_1.logError)(semanticError); return [ new result_1.Result({ @@ -56219,6 +56433,12 @@ async function runPullRequestWorkflow(param, taskId, ports) { } return []; } +function shouldUpdatePullRequestDescriptionAutomatically(param) { + const mode = param.ai.getPullRequestDescriptionMode?.(); + return mode === undefined + ? param.ai.getAiPullRequestDescription() + : mode === 'replace' || mode === 'append'; +} async function runPullRequestReview(param, ports) { if (!ports.reviewPotentialProblemsUseCase || !shouldReviewPullRequest(param)) return []; @@ -62146,6 +62366,15 @@ class UpdatePullRequestDescriptionUseCase { aiRepository: this.aiRepository, }); } + /** Explicit comment commands may update a preserved PR body on demand. */ + async invokeExplicit(param) { + return await (0, update_pull_request_description_workflow_1.runUpdatePullRequestDescriptionWorkflow)(param, this.taskId, { + pullRequestDescriptionCommandPort: this.pullRequestDescriptionCommandPort, + issueDescriptionQueryPort: this.issueDescriptionQueryPort, + organizationMembersPort: this.organizationMembersPort, + aiRepository: this.aiRepository, + }, true); + } } exports.UpdatePullRequestDescriptionUseCase = UpdatePullRequestDescriptionUseCase; @@ -62166,11 +62395,15 @@ const logging_ports_1 = __nccwpck_require__(6152); const project_context_instruction_1 = __nccwpck_require__(63907); const task_emoji_1 = __nccwpck_require__(46103); const github_comment_publication_policy_1 = __nccwpck_require__(72712); +const pull_request_description_1 = __nccwpck_require__(45315); +const application_error_1 = __nccwpck_require__(75999); /** Generates and publishes a PR description while keeping provider details behind ports. */ -async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependencies) { +async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependencies, force = false) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(taskId)} Executing ${taskId} (AI PR description).`); try { - const branches = getPullRequestBranches(param); + const pullRequestNumber = getPullRequestNumber(param); + const details = await loadPullRequestDetails(param, dependencies, pullRequestNumber, force); + const branches = getPullRequestBranches(param, details); if (!branches) { return [ new result_1.Result({ @@ -62183,6 +62416,10 @@ async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependenci }), ]; } + const mode = getPullRequestDescriptionMode(param); + if (mode === 'disabled' || (!force && !(0, pull_request_description_1.shouldAutomaticallyUpdatePullRequestDescription)(mode))) { + return skipped(taskId, `Automatic PR description updates are disabled by the "${mode}" mode.`); + } (0, logging_ports_1.logDebugInfo)(`PR description will be generated from workspace diff: base "${branches.baseBranch}", head "${branches.headBranch}" (configured agent will run git diff).`); const issueDescription = param.issueNumber > 0 ? (await dependencies.issueDescriptionQueryPort.getDescription(param.owner, param.repo, param.issueNumber, param.tokens.token)) ?? '' @@ -62212,31 +62449,54 @@ async function runUpdatePullRequestDescriptionWorkflow(param, taskId, dependenci agentId: agent_task_policy_1.AGENT_PLAN, prompt, }); - const pullRequestBody = (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(extractDescription(response)); + const generatedDescription = (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(extractDescription(response)); + const pullRequestBody = mode === 'replace' + ? generatedDescription + : (0, pull_request_description_1.mergeManagedPullRequestDescription)(details?.body ?? param.pullRequest.body, generatedDescription); (0, logging_ports_1.logDebugInfo)(`UpdatePullRequestDescription: agent response received. Description length=${pullRequestBody.length}.`); if (!pullRequestBody.trim()) { return newResult(taskId, false, true, ['Configured agent did not return a PR description.']); } - await dependencies.pullRequestDescriptionCommandPort.updateDescription(param.owner, param.repo, param.pullRequest.number, pullRequestBody, param.tokens.token); + await dependencies.pullRequestDescriptionCommandPort.updateDescription(param.owner, param.repo, pullRequestNumber, pullRequestBody, param.tokens.token); return [new result_1.Result({ id: taskId, success: true, executed: true, steps: [] })]; } - catch (error) { + catch (cause) { + const error = new application_error_1.ApplicationError('Unable to update pull request description.', 'workflow', { cause }); (0, logging_ports_1.logError)(error); return [ new result_1.Result({ id: taskId, success: false, executed: true, - steps: [`Error updating pull request description: ${error}`], + steps: [error.message], + errors: [error], }), ]; } } -function getPullRequestBranches(param) { - const headBranch = param.pullRequest.head; - const baseBranch = param.pullRequest.base; +function getPullRequestBranches(param, details) { + const headBranch = param.pullRequest.head || details?.headBranch; + const baseBranch = param.pullRequest.base || details?.baseBranch; return headBranch && baseBranch ? { headBranch, baseBranch } : undefined; } +function getPullRequestNumber(param) { + return param.pullRequest.number > 0 ? param.pullRequest.number : param.issue.number; +} +async function loadPullRequestDetails(param, dependencies, pullRequestNumber, force) { + if (pullRequestNumber <= 0 || !dependencies.pullRequestDescriptionCommandPort.getDetails) + return undefined; + const needsRemoteDetails = param.eventName === 'issue_comment' + || force + || !param.pullRequest.head + || !param.pullRequest.base; + if (!needsRemoteDetails) + return undefined; + return dependencies.pullRequestDescriptionCommandPort.getDetails(param.owner, param.repo, pullRequestNumber, param.tokens.token); +} +function getPullRequestDescriptionMode(param) { + return param.ai.getPullRequestDescriptionMode?.() + ?? (param.ai.getAiPullRequestDescription() ? 'replace' : 'disabled'); +} function extractDescription(response) { if (typeof response === 'string') return response; @@ -62381,11 +62641,12 @@ Object.defineProperty(exports, "isAgentConfigurationReady", ({ enumerable: true, Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Ai = void 0; const agent_command_1 = __nccwpck_require__(77923); +const pull_request_description_1 = __nccwpck_require__(45315); class Ai { constructor(_configurationSource, model, aiPullRequestDescription, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotMinSeverity, bugbotCommentLimit, bugbotFixVerifyCommands = [], agentTasks = { findings: { provider: 'codex', modelProvider: 'openai', model, command: (0, agent_command_1.defaultAgentCommand)({ provider: 'codex', modelProvider: 'openai', model }) }, fixer: { provider: 'codex', modelProvider: 'openai', model, command: (0, agent_command_1.defaultAgentCommand)({ provider: 'codex', modelProvider: 'openai', model }) }, - }) { + }, pullRequestDescriptionMode = pull_request_description_1.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE) { this.aiPullRequestDescription = aiPullRequestDescription; this.aiMembersOnly = aiMembersOnly; this.aiIgnoreFiles = aiIgnoreFiles; @@ -62394,10 +62655,14 @@ class Ai { this.bugbotCommentLimit = bugbotCommentLimit; this.bugbotFixVerifyCommands = bugbotFixVerifyCommands; this.agentTasks = agentTasks; + this.pullRequestDescriptionMode = (0, pull_request_description_1.normalizePullRequestDescriptionMode)(pullRequestDescriptionMode); } getAiPullRequestDescription() { return this.aiPullRequestDescription; } + getPullRequestDescriptionMode() { + return this.pullRequestDescriptionMode; + } getAiMembersOnly() { return this.aiMembersOnly; } @@ -62540,14 +62805,20 @@ exports.Commit = Commit; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.Config = void 0; +exports.Config = exports.CONFIG_SCHEMA_VERSION = void 0; const branch_configuration_1 = __nccwpck_require__(71934); const recommendation_state_1 = __nccwpck_require__(68514); const model_input_1 = __nccwpck_require__(14637); +exports.CONFIG_SCHEMA_VERSION = 1; class Config { constructor(data) { this.results = []; const input = (0, model_input_1.asModelInput)(data); + this.schemaVersion = typeof input.schemaVersion === 'number' + && Number.isInteger(input.schemaVersion) + && input.schemaVersion > 0 + ? input.schemaVersion + : exports.CONFIG_SCHEMA_VERSION; this.branchType = (0, model_input_1.readString)(input, 'branchType'); this.hotfixOriginBranch = (0, model_input_1.readOptionalString)(input, 'hotfixOriginBranch'); this.hotfixBranch = (0, model_input_1.readOptionalString)(input, 'hotfixBranch'); @@ -63360,7 +63631,11 @@ class PullRequest { return this.inputs?.pull_request?.user?.login ?? ''; } get number() { - return (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.inputs?.pull_request?.number) ?? -1; + return (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.inputs?.pull_request?.number) + ?? (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.inputs?.review?.pull_request?.number) + ?? uniquePullRequestNumber(this.inputs?.check_suite?.pull_requests) + ?? uniquePullRequestNumber(this.inputs?.workflow_run?.pull_requests) + ?? -1; } get url() { return this.inputs?.pull_request?.html_url ?? ''; @@ -63369,7 +63644,10 @@ class PullRequest { return this.inputs?.pull_request?.body ?? ''; } get head() { - return this.inputs?.pull_request?.head?.ref ?? ''; + return this.inputs?.pull_request?.head?.ref + ?? this.inputs?.check_suite?.head_branch + ?? this.inputs?.workflow_run?.head_branch + ?? ''; } get base() { return this.inputs?.pull_request?.base?.ref ?? ''; @@ -63392,7 +63670,12 @@ class PullRequest { return this.action === 'synchronize'; } get isPullRequest() { - return this.inputs?.eventName === 'pull_request'; + return [ + 'pull_request', + 'pull_request_review', + 'check_suite', + 'workflow_run', + ].includes(this.inputs?.eventName ?? ''); } get isPullRequestReviewComment() { return this.inputs?.eventName === 'pull_request_review_comment'; @@ -63427,6 +63710,11 @@ class PullRequest { } } exports.PullRequest = PullRequest; +function uniquePullRequestNumber(pullRequests) { + return pullRequests?.length === 1 + ? (0, positive_integer_policy_1.parsePositiveSafeInteger)(pullRequests[0]?.number) + : undefined; +} /***/ }), @@ -67767,6 +68055,21 @@ class PullRequestLifecycleRepository { }); (0, logger_1.logDebugInfo)(`Updated PR #${pullRequestNumber} description with: ${description}`); }; + this.getDetails = async (owner, repository, pullRequestNumber, token) => { + const octokit = this.githubClient.getClient(token); + if (!octokit.rest.pulls.get) + throw new Error('Pull-request details query is not available.'); + const { data } = await octokit.rest.pulls.get({ + owner, + repo: repository, + pull_number: pullRequestNumber, + }); + return { + body: data.body ?? '', + headBranch: data.head?.ref ?? '', + baseBranch: data.base?.ref ?? '', + }; + }; } async listOpenPullRequests(octokit, owner, repository, filters = {}) { const allPullRequests = []; @@ -69073,6 +69376,7 @@ exports.COPILOT_COMMAND_NAMES = [ 'estimate', 'test-plan', 'status', + 'description', 'review', 'findings', 'fix', @@ -69283,6 +69587,65 @@ function parsePositiveSafeInteger(value) { } +/***/ }), + +/***/ 45315: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.MANAGED_PULL_REQUEST_DESCRIPTION_END = exports.MANAGED_PULL_REQUEST_DESCRIPTION_START = exports.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE = exports.PULL_REQUEST_DESCRIPTION_MODES = void 0; +exports.normalizePullRequestDescriptionMode = normalizePullRequestDescriptionMode; +exports.hasManagedPullRequestDescription = hasManagedPullRequestDescription; +exports.renderManagedPullRequestDescription = renderManagedPullRequestDescription; +exports.mergeManagedPullRequestDescription = mergeManagedPullRequestDescription; +exports.shouldAutomaticallyUpdatePullRequestDescription = shouldAutomaticallyUpdatePullRequestDescription; +exports.PULL_REQUEST_DESCRIPTION_MODES = [ + 'replace', + 'append', + 'preserve', + 'disabled', +]; +exports.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE = 'replace'; +exports.MANAGED_PULL_REQUEST_DESCRIPTION_START = ''; +exports.MANAGED_PULL_REQUEST_DESCRIPTION_END = ''; +/** Normalizes public configuration while keeping invalid values safe and backwards compatible. */ +function normalizePullRequestDescriptionMode(value) { + const normalized = String(value ?? '').trim().toLowerCase(); + return exports.PULL_REQUEST_DESCRIPTION_MODES.includes(normalized) + ? normalized + : exports.DEFAULT_PULL_REQUEST_DESCRIPTION_MODE; +} +function hasManagedPullRequestDescription(body) { + return typeof body === 'string' && body.includes(exports.MANAGED_PULL_REQUEST_DESCRIPTION_START); +} +/** Renders one bounded Copilot-owned section without taking ownership of the rest of the body. */ +function renderManagedPullRequestDescription(generated) { + return [ + exports.MANAGED_PULL_REQUEST_DESCRIPTION_START, + generated.trim(), + exports.MANAGED_PULL_REQUEST_DESCRIPTION_END, + ].join('\n'); +} +/** Replaces the existing managed section, or appends one when none exists. */ +function mergeManagedPullRequestDescription(currentBody, generated) { + const current = typeof currentBody === 'string' ? currentBody.trim() : ''; + const managed = renderManagedPullRequestDescription(generated); + const start = current.indexOf(exports.MANAGED_PULL_REQUEST_DESCRIPTION_START); + const end = current.indexOf(exports.MANAGED_PULL_REQUEST_DESCRIPTION_END, start + exports.MANAGED_PULL_REQUEST_DESCRIPTION_START.length); + if (start >= 0 && end >= start) { + const before = current.slice(0, start).trimEnd(); + const after = current.slice(end + exports.MANAGED_PULL_REQUEST_DESCRIPTION_END.length).trimStart(); + return [before, managed, after].filter(Boolean).join('\n\n').trim(); + } + return current ? `${current}\n\n${managed}` : managed; +} +function shouldAutomaticallyUpdatePullRequestDescription(mode) { + return mode === 'replace' || mode === 'append'; +} + + /***/ }), /***/ 67057: @@ -69991,7 +70354,6 @@ const check_pull_request_comment_language_use_case_1 = __nccwpck_require__(21729 const comment_language_translation_workflow_1 = __nccwpck_require__(72770); const branch_compare_repository_1 = __nccwpck_require__(95859); const merge_repository_1 = __nccwpck_require__(31412); -const pull_request_lifecycle_repository_1 = __nccwpck_require__(24189); const repository_release_publication_repository_1 = __nccwpck_require__(42075); const repository_tag_repository_1 = __nccwpck_require__(58717); const git_commit_adapter_1 = __nccwpck_require__(18606); @@ -70009,6 +70371,9 @@ const issue_interaction_composition_root_1 = __nccwpck_require__(92503); const issue_labels_composition_root_1 = __nccwpck_require__(34780); const issue_use_case_composition_root_1 = __nccwpck_require__(43022); const pull_request_use_case_composition_root_1 = __nccwpck_require__(70636); +const organization_members_composition_root_1 = __nccwpck_require__(50603); +const update_pull_request_description_use_case_1 = __nccwpck_require__(75089); +const pull_request_lifecycle_repository_1 = __nccwpck_require__(24189); function createDetectPotentialProblemsUseCase() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); return new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)(), bugbot.context, bugbot.publication, bugbot.resolution); @@ -70025,7 +70390,8 @@ function createIssueCommentUseCaseCompositionRoot() { const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); - return new issue_comment_use_case_1.IssueCommentUseCase(new check_issue_comment_language_use_case_1.CheckIssueCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution)); + const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); + return new issue_comment_use_case_1.IssueCommentUseCase(new check_issue_comment_language_use_case_1.CheckIssueCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution), pullRequestDescription); } function createPullRequestReviewCommentUseCaseCompositionRoot() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); @@ -70033,21 +70399,34 @@ function createPullRequestReviewCommentUseCaseCompositionRoot() { const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); - return new pull_request_review_comment_use_case_1.PullRequestReviewCommentUseCase(new check_pull_request_comment_language_use_case_1.CheckPullRequestCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution)); + const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); + return new pull_request_review_comment_use_case_1.PullRequestReviewCommentUseCase(new check_pull_request_comment_language_use_case_1.CheckPullRequestCommentLanguageUseCase(new comment_language_translation_workflow_1.CommentLanguageTranslationWorkflow(bugbot.issue, language)), new detect_bugbot_fix_intent_use_case_1.DetectBugbotFixIntentUseCase(bugbot.context.pullRequest, findings, bugbot.context), new think_use_case_1.ThinkUseCase((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), findings), new bugbot_autofix_use_case_1.BugbotAutofixUseCase(fixer, bugbot.context, gitCommit), new user_request_use_case_1.DoUserRequestUseCase(fixer), bugbot.issue, (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(), bugbot.resolution, gitCommit, new dismiss_bugbot_findings_use_case_1.DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution), pullRequestDescription); } function createCommitUseCaseCompositionRoot(projectBoardCommandPort) { return new commit_use_case_1.CommitUseCase(new notify_new_commit_on_issue_use_case_1.NotifyNewCommitOnIssueUseCase((0, issue_interaction_composition_root_1.createIssueNotificationRepository)()), new check_changes_issue_size_use_case_1.CheckChangesIssueSizeUseCase(projectBoardCommandPort, (0, issue_labels_composition_root_1.createIssueLabelRepository)(), new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)())), createDetectPotentialProblemsUseCase(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)()); } function createMainRunRouteCompositionRoot(projectBoardCommandPort) { + // Composition is scoped to one main run. Each route is built only when it is + // actually selected, while repeated calls in the same run reuse its graph. + const singleAction = lazy(() => createSingleActionUseCaseCompositionRoot()); + const issueComment = lazy(() => createIssueCommentUseCaseCompositionRoot()); + const issue = lazy(() => (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)()); + const pullRequestReviewComment = lazy(() => createPullRequestReviewCommentUseCaseCompositionRoot()); + const pullRequest = lazy(() => (0, pull_request_use_case_composition_root_1.createPullRequestUseCaseCompositionRoot)()); + const push = lazy(() => createCommitUseCaseCompositionRoot(projectBoardCommandPort)); return { - "single-action": async (execution) => createSingleActionUseCaseCompositionRoot().invoke(execution), - "issue-comment": async (execution) => createIssueCommentUseCaseCompositionRoot().invoke(execution), - issue: async (execution) => (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)().invoke(execution), - "pull-request-review-comment": async (execution) => createPullRequestReviewCommentUseCaseCompositionRoot().invoke(execution), - "pull-request": async (execution) => (0, pull_request_use_case_composition_root_1.createPullRequestUseCaseCompositionRoot)().invoke(execution), - push: async (execution) => createCommitUseCaseCompositionRoot(projectBoardCommandPort).invoke(execution), + "single-action": async (execution) => singleAction().invoke(execution), + "issue-comment": async (execution) => issueComment().invoke(execution), + issue: async (execution) => issue().invoke(execution), + "pull-request-review-comment": async (execution) => pullRequestReviewComment().invoke(execution), + "pull-request": async (execution) => pullRequest().invoke(execution), + push: async (execution) => push().invoke(execution), }; } +function lazy(factory) { + let value; + return () => value ?? (value = factory()); +} /***/ }), @@ -71035,15 +71414,17 @@ exports.ConfigurationHandler = ConfigurationHandler; /***/ }), /***/ 58043: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildConfigurationPayload = buildConfigurationPayload; +const config_1 = __nccwpck_require__(90450); function buildConfigurationPayload(execution, storedRaw) { const current = execution.currentConfiguration; const payload = { + schemaVersion: config_1.CONFIG_SCHEMA_VERSION, branchType: current.branchType, releaseBranch: current.releaseBranch, workingBranch: current.workingBranch, @@ -71925,6 +72306,7 @@ exports.INPUT_KEYS = { RELEASE_COMMAND: 'release-command', // AI configuration AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', + AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', AI_MEMBERS_ONLY: 'ai-members-only', AI_IGNORE_FILES: 'ai-ignore-files', AI_INCLUDE_REASONING: 'ai-include-reasoning', diff --git a/build/github_action/src/actions/github_action_ai_inputs.d.ts b/build/github_action/src/actions/github_action_ai_inputs.d.ts index a1a5d779..0a527f9e 100644 --- a/build/github_action/src/actions/github_action_ai_inputs.d.ts +++ b/build/github_action/src/actions/github_action_ai_inputs.d.ts @@ -1,7 +1,9 @@ import type { AgentTaskConfiguration } from '../data/model/agent'; +import { type PullRequestDescriptionMode } from '../domain/pull_request_description'; export interface GithubActionAiInputs { readonly requestedAgentTasks: AgentTaskConfiguration; readonly pullRequestDescription: boolean; + readonly pullRequestDescriptionMode: PullRequestDescriptionMode; readonly membersOnly: boolean; readonly includeReasoning: boolean; readonly ignoreFiles: string[]; diff --git a/build/github_action/src/actions/local_action_configuration.d.ts b/build/github_action/src/actions/local_action_configuration.d.ts index 748e9222..f6ab15af 100644 --- a/build/github_action/src/actions/local_action_configuration.d.ts +++ b/build/github_action/src/actions/local_action_configuration.d.ts @@ -115,6 +115,7 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn agentTasks: import("../domain/agent").AgentTaskConfiguration; agentModel: string; aiPullRequestDescription: boolean; + aiPullRequestDescriptionMode: "replace" | "append" | "preserve" | "disabled"; aiMembersOnly: boolean; aiIncludeReasoning: boolean; aiIgnoreFilesInput: string; diff --git a/build/github_action/src/actions/local_action_configuration_sections.d.ts b/build/github_action/src/actions/local_action_configuration_sections.d.ts index 740ed554..7b0d3a20 100644 --- a/build/github_action/src/actions/local_action_configuration_sections.d.ts +++ b/build/github_action/src/actions/local_action_configuration_sections.d.ts @@ -18,6 +18,7 @@ export declare function readLocalAgentConfiguration(additionalParams: ActionInpu agentTasks: import("../domain/agent").AgentTaskConfiguration; agentModel: string; aiPullRequestDescription: boolean; + aiPullRequestDescriptionMode: "replace" | "append" | "preserve" | "disabled"; aiMembersOnly: boolean; aiIncludeReasoning: boolean; aiIgnoreFilesInput: string; diff --git a/build/github_action/src/application/errors/application_error.d.ts b/build/github_action/src/application/errors/application_error.d.ts new file mode 100644 index 00000000..69ed4cca --- /dev/null +++ b/build/github_action/src/application/errors/application_error.d.ts @@ -0,0 +1,13 @@ +export type ApplicationErrorKind = 'configuration' | 'authorization' | 'provider' | 'agent' | 'validation' | 'workflow' | 'unknown'; +export interface ApplicationErrorOptions { + readonly retryable?: boolean; + readonly cause?: unknown; +} +/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ +export declare class ApplicationError extends Error { + readonly kind: ApplicationErrorKind; + readonly retryable: boolean; + readonly cause?: unknown; + constructor(message: string, kind?: ApplicationErrorKind, options?: ApplicationErrorOptions); +} +export declare function toApplicationError(error: unknown, message: string, kind?: ApplicationErrorKind, options?: ApplicationErrorOptions): ApplicationError; diff --git a/build/github_action/src/application/policies/action_summary_policy.d.ts b/build/github_action/src/application/policies/action_summary_policy.d.ts index aeb86812..edd3e792 100644 --- a/build/github_action/src/application/policies/action_summary_policy.d.ts +++ b/build/github_action/src/application/policies/action_summary_policy.d.ts @@ -6,6 +6,7 @@ export interface ActionSummaryContext { readonly issueNumber: number; readonly pullRequestNumber: number; readonly lifecycleState?: string; + readonly pullRequestDescriptionMode?: string; readonly results: readonly Result[]; } /** Builds a bounded, publication-safe GitHub Actions Job Summary. */ diff --git a/build/github_action/src/application/policies/lifecycle_state_policy.d.ts b/build/github_action/src/application/policies/lifecycle_state_policy.d.ts index 8f63269a..e8c94567 100644 --- a/build/github_action/src/application/policies/lifecycle_state_policy.d.ts +++ b/build/github_action/src/application/policies/lifecycle_state_policy.d.ts @@ -1,4 +1,11 @@ import type { CopilotLifecycleState } from '../../domain/copilot_lifecycle'; +import type { ExecutionInputs } from '../../data/model/execution_inputs'; +export type LifecycleChecksEvidence = 'pending' | 'success' | 'failure'; +export type LifecycleReviewEvidence = 'approved' | 'changes-requested' | 'commented' | 'dismissed'; +export interface LifecycleExternalEvidence { + readonly checks?: LifecycleChecksEvidence; + readonly review?: LifecycleReviewEvidence; +} export interface LifecycleStatePolicyResult { readonly id: string; readonly success: boolean; @@ -16,7 +23,10 @@ export interface LifecycleStateDecisionInput { readonly issueDescriptionEdited: boolean; readonly pullRequestMerged: boolean; readonly pullRequestClosed: boolean; + readonly externalEvidence?: LifecycleExternalEvidence; readonly results: readonly LifecycleStatePolicyResult[]; } /** Resolves the next lifecycle state from application facts, never from labels or API responses. */ export declare function resolveLifecycleState(input: LifecycleStateDecisionInput): CopilotLifecycleState | undefined; +/** Extracts only stable review/check facts from GitHub event payloads. */ +export declare function readLifecycleExternalEvidence(inputs: ExecutionInputs | undefined): LifecycleExternalEvidence | undefined; diff --git a/build/github_action/src/application/policies/status_command_policy.d.ts b/build/github_action/src/application/policies/status_command_policy.d.ts new file mode 100644 index 00000000..b6ae9682 --- /dev/null +++ b/build/github_action/src/application/policies/status_command_policy.d.ts @@ -0,0 +1,26 @@ +import type { Execution } from '../../data/model/execution'; +import { Result } from '../../data/model/result'; +export interface CopilotStatusSnapshot { + readonly owner: string; + readonly repository: string; + readonly event: string; + readonly action: string; + readonly target: 'issue' | 'pull-request' | 'push' | 'repository'; + readonly issueNumber?: number; + readonly pullRequestNumber?: number; + readonly branch?: string; + readonly lifecycle?: string; + readonly waitingFor?: string; + readonly issueLabels: readonly string[]; + readonly pullRequestLabels: readonly string[]; + readonly activeFindings?: { + open: number; + reopened: number; + resolved: number; + }; + readonly pullRequestDescriptionMode: string; +} +/** Builds a read-only status snapshot from the facts already loaded by setup. */ +export declare function buildCopilotStatusSnapshot(execution: Execution): CopilotStatusSnapshot; +export declare function buildCopilotStatusResult(execution: Execution, taskId: string): Result; +export declare function formatCopilotStatus(snapshot: CopilotStatusSnapshot): string; diff --git a/build/github_action/src/application/ports/execution_resolution_ports.d.ts b/build/github_action/src/application/ports/execution_resolution_ports.d.ts index 9452fb7e..d0a300cf 100644 --- a/build/github_action/src/application/ports/execution_resolution_ports.d.ts +++ b/build/github_action/src/application/ports/execution_resolution_ports.d.ts @@ -17,6 +17,7 @@ export interface ExecutionIssueResolutionContext { }; pullRequest: { head: string; + number?: number; }; commit: { branch: string; diff --git a/build/github_action/src/application/ports/pull_request_description_ports.d.ts b/build/github_action/src/application/ports/pull_request_description_ports.d.ts index 8b675524..dfa4768f 100644 --- a/build/github_action/src/application/ports/pull_request_description_ports.d.ts +++ b/build/github_action/src/application/ports/pull_request_description_ports.d.ts @@ -1,3 +1,10 @@ +export interface PullRequestDescriptionDetails { + readonly body: string; + readonly headBranch: string; + readonly baseBranch: string; +} export interface PullRequestDescriptionCommandPort { updateDescription(owner: string, repository: string, pullRequestNumber: number, description: string, token: string): Promise; + /** Optional read capability used by explicit commands from issue comments. */ + getDetails?(owner: string, repository: string, pullRequestNumber: number, token: string): Promise; } diff --git a/build/github_action/src/application/usecases/comment_automation_contracts.d.ts b/build/github_action/src/application/usecases/comment_automation_contracts.d.ts index 9e9a75b1..de79d5a8 100644 --- a/build/github_action/src/application/usecases/comment_automation_contracts.d.ts +++ b/build/github_action/src/application/usecases/comment_automation_contracts.d.ts @@ -5,6 +5,9 @@ import type { BugbotAutofixParam } from "./steps/commit/bugbot/bugbot_autofix_us import type { DoUserRequestParam } from "./steps/commit/user_request_use_case"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +export interface ExplicitPullRequestDescriptionUseCase { + invokeExplicit(param: Execution): Promise; +} export interface CommentAutomationOptions { taskId: string; languageUseCase: ParamUseCase; @@ -17,4 +20,6 @@ export interface CommentAutomationOptions { userComment: string; gitCommitPort: GitCommitPort; dismissBugbotFindingsUseCase?: ParamUseCase; + /** Optional explicit PR description command; automatic PR updates remain a separate route. */ + updatePullRequestDescriptionUseCase?: ExplicitPullRequestDescriptionUseCase; } diff --git a/build/github_action/src/application/usecases/issue_comment_use_case.d.ts b/build/github_action/src/application/usecases/issue_comment_use_case.d.ts index b5dddcc6..04efabe4 100644 --- a/build/github_action/src/application/usecases/issue_comment_use_case.d.ts +++ b/build/github_action/src/application/usecases/issue_comment_use_case.d.ts @@ -9,6 +9,7 @@ import type { ActorAuthorizationPort } from "../ports/actor_authorization_ports" import type { BugbotFindingResolutionPorts } from "../ports/bugbot_finding_resolution_ports"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +import type { UpdatePullRequestDescriptionUseCase } from './steps/pull_request/update_pull_request_description_use_case'; export declare class IssueCommentUseCase implements ParamUseCase { private readonly languageUseCase; private readonly intentUseCase; @@ -22,7 +23,8 @@ export declare class IssueCommentUseCase implements ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined); + constructor(languageUseCase: ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined, updatePullRequestDescriptionUseCase?: UpdatePullRequestDescriptionUseCase | undefined); invoke(param: Execution): Promise; } diff --git a/build/github_action/src/application/usecases/pull_request_review_comment_use_case.d.ts b/build/github_action/src/application/usecases/pull_request_review_comment_use_case.d.ts index ad194518..e7cba57e 100644 --- a/build/github_action/src/application/usecases/pull_request_review_comment_use_case.d.ts +++ b/build/github_action/src/application/usecases/pull_request_review_comment_use_case.d.ts @@ -9,6 +9,7 @@ import type { ActorAuthorizationPort } from "../ports/actor_authorization_ports" import type { BugbotFindingResolutionPorts } from "../ports/bugbot_finding_resolution_ports"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +import type { UpdatePullRequestDescriptionUseCase } from './steps/pull_request/update_pull_request_description_use_case'; export declare class PullRequestReviewCommentUseCase implements ParamUseCase { private readonly languageUseCase; private readonly intentUseCase; @@ -22,7 +23,8 @@ export declare class PullRequestReviewCommentUseCase implements ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined); + constructor(languageUseCase: ParamUseCase, intentUseCase: ParamUseCase, thinkUseCase: ParamUseCase, autofixUseCase: ParamUseCase, doUserRequestUseCase: ParamUseCase, issueCommentUpdatePort: IssueCommentUpdatePort, actorAuthorizationPort: ActorAuthorizationPort, authenticatedUserPort: AuthenticatedUserPort, bugbotResolutionPorts: BugbotFindingResolutionPorts, gitCommitPort: GitCommitPort, dismissBugbotFindingsUseCase?: ParamUseCase | undefined, reviewPotentialProblemsUseCase?: ParamUseCase | undefined, updatePullRequestDescriptionUseCase?: UpdatePullRequestDescriptionUseCase | undefined); invoke(param: Execution): Promise; } diff --git a/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts b/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts index 738c1f94..bd583633 100644 --- a/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts +++ b/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.d.ts @@ -14,4 +14,6 @@ export declare class UpdatePullRequestDescriptionUseCase implements ParamUseCase taskId: string; constructor(pullRequestDescriptionCommandPort: PullRequestDescriptionCommandPort, issueDescriptionQueryPort: IssueDescriptionQueryPort, organizationMembersPort: OrganizationMembersPort, aiRepository: FindingsQueryPort); invoke(param: Execution): Promise; + /** Explicit comment commands may update a preserved PR body on demand. */ + invokeExplicit(param: Execution): Promise; } diff --git a/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts b/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts index 0d5633da..57c83a9f 100644 --- a/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts +++ b/build/github_action/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.d.ts @@ -11,4 +11,4 @@ export interface UpdatePullRequestDescriptionWorkflowDependencies { aiRepository: FindingsQueryPort; } /** Generates and publishes a PR description while keeping provider details behind ports. */ -export declare function runUpdatePullRequestDescriptionWorkflow(param: Execution, taskId: string, dependencies: UpdatePullRequestDescriptionWorkflowDependencies): Promise; +export declare function runUpdatePullRequestDescriptionWorkflow(param: Execution, taskId: string, dependencies: UpdatePullRequestDescriptionWorkflowDependencies, force?: boolean): Promise; diff --git a/build/github_action/src/cli/commands/reconcile.d.ts b/build/github_action/src/cli/commands/reconcile.d.ts new file mode 100644 index 00000000..b9a89bbc --- /dev/null +++ b/build/github_action/src/cli/commands/reconcile.d.ts @@ -0,0 +1,10 @@ +import { Command } from 'commander'; +import type { SetupWorkspacePort } from '../../application/ports/setup_workspace_ports'; +export interface ReconcileCommandOptions { + config?: string; + apply?: boolean; + json?: boolean; +} +/** Reconciles setup-managed workflow files locally; remote GitHub state is never changed. */ +export declare function registerReconcileCommand(program: Command): void; +export declare function runReconcileCommand(options: ReconcileCommandOptions, workspace?: SetupWorkspacePort): void; diff --git a/build/github_action/src/data/model/ai.d.ts b/build/github_action/src/data/model/ai.d.ts index 68ae90c9..759c99a2 100644 --- a/build/github_action/src/data/model/ai.d.ts +++ b/build/github_action/src/data/model/ai.d.ts @@ -1,4 +1,5 @@ import { AgentConfiguration, AgentTask, AgentTaskConfiguration } from './agent'; +import { type PullRequestDescriptionMode } from '../../domain/pull_request_description'; export declare class Ai { private aiPullRequestDescription; private aiMembersOnly; @@ -8,8 +9,10 @@ export declare class Ai { private bugbotCommentLimit; private bugbotFixVerifyCommands; private agentTasks; - constructor(_configurationSource: string, model: string, aiPullRequestDescription: boolean, aiMembersOnly: boolean, aiIgnoreFiles: string[], aiIncludeReasoning: boolean, bugbotMinSeverity: string, bugbotCommentLimit: number, bugbotFixVerifyCommands?: string[], agentTasks?: AgentTaskConfiguration); + private pullRequestDescriptionMode; + constructor(_configurationSource: string, model: string, aiPullRequestDescription: boolean, aiMembersOnly: boolean, aiIgnoreFiles: string[], aiIncludeReasoning: boolean, bugbotMinSeverity: string, bugbotCommentLimit: number, bugbotFixVerifyCommands?: string[], agentTasks?: AgentTaskConfiguration, pullRequestDescriptionMode?: PullRequestDescriptionMode); getAiPullRequestDescription(): boolean; + getPullRequestDescriptionMode(): PullRequestDescriptionMode; getAiMembersOnly(): boolean; getAiIgnoreFiles(): string[]; getAiIncludeReasoning(): boolean; diff --git a/build/github_action/src/data/model/config.d.ts b/build/github_action/src/data/model/config.d.ts index bdab7e9f..3efd7156 100644 --- a/build/github_action/src/data/model/config.d.ts +++ b/build/github_action/src/data/model/config.d.ts @@ -1,7 +1,9 @@ import { BranchConfiguration } from "./branch_configuration"; import { RecommendationState } from "./recommendation_state"; import { Result } from "./result"; +export declare const CONFIG_SCHEMA_VERSION = 1; export declare class Config { + readonly schemaVersion: number; branchType: string; releaseBranch: string | undefined; workingBranch: string | undefined; diff --git a/build/github_action/src/data/model/execution_inputs.d.ts b/build/github_action/src/data/model/execution_inputs.d.ts index 899e3b71..b61005ab 100644 --- a/build/github_action/src/data/model/execution_inputs.d.ts +++ b/build/github_action/src/data/model/execution_inputs.d.ts @@ -35,6 +35,25 @@ export interface EventPullRequestPayload { merged?: boolean; state?: string; } +export interface EventPullRequestReferencePayload { + number?: number; +} +export interface EventReviewPayload { + state?: string; + pull_request?: EventPullRequestReferencePayload; +} +export interface EventCheckSuitePayload { + status?: string; + conclusion?: string | null; + head_branch?: string; + pull_requests?: EventPullRequestReferencePayload[]; +} +export interface EventWorkflowRunPayload { + status?: string; + conclusion?: string | null; + head_branch?: string; + pull_requests?: EventPullRequestReferencePayload[]; +} export interface EventCommitPayload { id?: string; message?: string; @@ -55,6 +74,9 @@ export interface ExecutionInputs { issue?: EventIssuePayload; label?: EventLabelPayload; pull_request?: EventPullRequestPayload; + review?: EventReviewPayload; + check_suite?: EventCheckSuitePayload; + workflow_run?: EventWorkflowRunPayload; comment?: EventCommentPayload; pull_request_review_comment?: EventCommentPayload; changes?: Record; diff --git a/build/github_action/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts b/build/github_action/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts index 1e4985f5..033b3bce 100644 --- a/build/github_action/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts +++ b/build/github_action/src/data/repository/pull_request/pull_request_lifecycle_repository.d.ts @@ -1,3 +1,4 @@ +import type { PullRequestDescriptionDetails } from '../../../application/ports/pull_request_description_ports'; import type { GithubClientPort } from "../../../infrastructure/github/ports/github_client_provider_port"; import type { GithubPullRequestLifecycleClient } from "../../../infrastructure/github/ports/github_pull_request_provider_ports"; export declare class PullRequestLifecycleRepository { @@ -21,4 +22,5 @@ export declare class PullRequestLifecycleRepository { isLinked: (pullRequestUrl: string) => Promise; updateBaseBranch: (owner: string, repository: string, pullRequestNumber: number, branch: string, token: string) => Promise; updateDescription: (owner: string, repository: string, pullRequestNumber: number, description: string, token: string) => Promise; + getDetails: (owner: string, repository: string, pullRequestNumber: number, token: string) => Promise; } diff --git a/build/github_action/src/domain/copilot_command.d.ts b/build/github_action/src/domain/copilot_command.d.ts index 25644aeb..6487f6f4 100644 --- a/build/github_action/src/domain/copilot_command.d.ts +++ b/build/github_action/src/domain/copilot_command.d.ts @@ -1,5 +1,5 @@ /** Explicit commands are the safe, deterministic entry point for mutations. */ -export declare const COPILOT_COMMAND_NAMES: readonly ["plan", "clarify", "estimate", "test-plan", "status", "review", "findings", "fix", "dismiss", "recheck"]; +export declare const COPILOT_COMMAND_NAMES: readonly ["plan", "clarify", "estimate", "test-plan", "status", "description", "review", "findings", "fix", "dismiss", "recheck"]; export type CopilotCommandName = typeof COPILOT_COMMAND_NAMES[number]; export interface ParsedCopilotCommand { readonly name: CopilotCommandName; diff --git a/build/github_action/src/domain/pull_request_description.d.ts b/build/github_action/src/domain/pull_request_description.d.ts new file mode 100644 index 00000000..3294625e --- /dev/null +++ b/build/github_action/src/domain/pull_request_description.d.ts @@ -0,0 +1,13 @@ +export declare const PULL_REQUEST_DESCRIPTION_MODES: readonly ["replace", "append", "preserve", "disabled"]; +export type PullRequestDescriptionMode = typeof PULL_REQUEST_DESCRIPTION_MODES[number]; +export declare const DEFAULT_PULL_REQUEST_DESCRIPTION_MODE: PullRequestDescriptionMode; +export declare const MANAGED_PULL_REQUEST_DESCRIPTION_START = ""; +export declare const MANAGED_PULL_REQUEST_DESCRIPTION_END = ""; +/** Normalizes public configuration while keeping invalid values safe and backwards compatible. */ +export declare function normalizePullRequestDescriptionMode(value: unknown): PullRequestDescriptionMode; +export declare function hasManagedPullRequestDescription(body: unknown): boolean; +/** Renders one bounded Copilot-owned section without taking ownership of the rest of the body. */ +export declare function renderManagedPullRequestDescription(generated: string): string; +/** Replaces the existing managed section, or appends one when none exists. */ +export declare function mergeManagedPullRequestDescription(currentBody: unknown, generated: string): string; +export declare function shouldAutomaticallyUpdatePullRequestDescription(mode: PullRequestDescriptionMode): boolean; diff --git a/build/github_action/src/domain/setup.d.ts b/build/github_action/src/domain/setup.d.ts index e02a2dcf..668d18ad 100644 --- a/build/github_action/src/domain/setup.d.ts +++ b/build/github_action/src/domain/setup.d.ts @@ -1,4 +1,5 @@ import type { AgentProvider, AgentTask } from './agent'; +import type { PullRequestDescriptionMode } from './pull_request_description'; export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'issueTemplates' | 'pullRequestTemplate'; export interface SetupFeatures { [feature: string]: boolean; @@ -30,6 +31,8 @@ export interface SetupRepositoryConfiguration { } export interface SetupAiConfiguration { pullRequestDescription: boolean; + /** Optional for backwards-compatible setup files created before v3.3.0. */ + pullRequestDescriptionMode?: PullRequestDescriptionMode; ignoreFiles: string; membersOnly: boolean; includeReasoning: boolean; diff --git a/build/github_action/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts b/build/github_action/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts index 5c79941c..a82e2d16 100644 --- a/build/github_action/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts +++ b/build/github_action/src/infrastructure/github/ports/github_pull_request_provider_ports.d.ts @@ -35,6 +35,17 @@ export interface GithubPullRequestLifecycleClient { data: GithubPullRequestSummary[]; }>; update(parameters: Record): Promise; + get?(parameters: Record): Promise<{ + data: { + body?: string | null; + head?: { + ref?: string | null; + }; + base?: { + ref?: string | null; + }; + }; + }>; }; }; } diff --git a/build/github_action/src/utils/constants.d.ts b/build/github_action/src/utils/constants.d.ts index 43f98677..14f23fdc 100644 --- a/build/github_action/src/utils/constants.d.ts +++ b/build/github_action/src/utils/constants.d.ts @@ -90,6 +90,7 @@ export declare const INPUT_KEYS: { readonly RELEASE_MODEL: "release-model"; readonly RELEASE_COMMAND: "release-command"; readonly AI_PULL_REQUEST_DESCRIPTION: "ai-pull-request-description"; + readonly AI_PULL_REQUEST_DESCRIPTION_MODE: "ai-pull-request-description-mode"; readonly AI_MEMBERS_ONLY: "ai-members-only"; readonly AI_IGNORE_FILES: "ai-ignore-files"; readonly AI_INCLUDE_REASONING: "ai-include-reasoning"; diff --git a/docs/agents/cli-commands.mdx b/docs/agents/cli-commands.mdx index e821e2d9..d98b8d99 100644 --- a/docs/agents/cli-commands.mdx +++ b/docs/agents/cli-commands.mdx @@ -23,6 +23,7 @@ Comments beginning with `/copilot` use a deterministic command boundary. Text el | `/copilot estimate` | Estimate effort, risk, and likely validation work. | No | | `/copilot test-plan` | Propose a focused test strategy. | No | | `/copilot status` | Summarize the current lifecycle and next step. | No | +| `/copilot description` | Generate or refresh the PR description using the configured policy, on explicit request. | Yes, PR body only | | `/copilot review` | Review the current issue or PR context. | No | | `/copilot findings` | Summarize known Bugbot findings. | No | | `/copilot recheck` | Request a fresh analysis of the current context. | No | diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 64e2bf8a..79dfe66c 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -30,6 +30,7 @@ Copilot provides extensive configuration options to customize your workflow. Use ## AI Features - `ai-pull-request-description`: Enable AI-powered automatic updates for pull request descriptions (default: "true"). When enabled, the configured agent CLI fills your repository's pull request template (`.github/pull_request_template.md`) using the issue description and the branch diff. See [Pull Requests → AI-generated PR description](/pull-requests/ai-description). + - `ai-pull-request-description-mode`: Select the PR body policy: `replace` owns the complete generated body (legacy behavior), `append` preserves human text and maintains a Copilot-managed section, `preserve` never updates automatically but allows `/copilot description`, and `disabled` never changes the body. Defaults to `replace`. - `issues-locale`: Target locale for issue comments (default: "en-US"). When comments are in another language, the configured agent translates them to this locale. See [Agent CLI configuration](/agents/cli-configuration). - `pull-requests-locale`: Target locale for PR review comments (default: "en-US"). Same translation behavior as `issues-locale` but for PR comments. - `ai-ignore-files`: Comma-separated list of paths to ignore for AI operations (e.g. progress detection, Bugbot; not used for PR description, where the agent computes the diff in the workspace). diff --git a/docs/pull-requests/ai-description.mdx b/docs/pull-requests/ai-description.mdx index 434ebf92..eca5fa52 100644 --- a/docs/pull-requests/ai-description.mdx +++ b/docs/pull-requests/ai-description.mdx @@ -38,6 +38,21 @@ If you don't have a template, the agent will still produce a structured descript - The action runs on the same `pull_request` events as the rest of the PR pipeline (e.g. opened, edited). +## Description policies + +Use `ai-pull-request-description-mode` when a team needs a different ownership model for the PR body: + +| Mode | Automatic runs | `/copilot description` | Behavior | +| --- | --- | --- | --- | +| `replace` | Yes | Yes | Preserves the existing legacy behavior: the generated text owns the complete body. | +| `append` | Yes | Yes | Keeps human-authored text and replaces only the bounded Copilot-managed section. | +| `preserve` | No | Yes | Does not touch the body automatically; a maintainer can request an update explicitly from a PR comment. | +| `disabled` | No | No | Description automation is fully disabled. | + +The mode can be set directly on the Action input or through the `AI_PULL_REQUEST_DESCRIPTION_MODE` Repository Variable written by `copilot setup`. The legacy boolean remains supported: setting `ai-pull-request-description: false` resolves to `disabled`. + +For an explicit update, comment `/copilot description` on the PR or its issue conversation. The command fetches the current PR body and branch metadata, so it also works from an `issue_comment` workflow. + ## Enable in your workflow Set `ai-pull-request-description: true` and configure the selected agent in your workflow: @@ -48,6 +63,7 @@ Set `ai-pull-request-description: true` and configure the selected agent in your token: ${{ secrets.PAT }} project-ids: '2,3' ai-pull-request-description: true + ai-pull-request-description-mode: append agent-provider: codex agent-model-provider: openai agent-model: gpt-5.6-luna diff --git a/docs/pull-requests/configuration.mdx b/docs/pull-requests/configuration.mdx index c5d98e37..aaa35fe6 100644 --- a/docs/pull-requests/configuration.mdx +++ b/docs/pull-requests/configuration.mdx @@ -21,6 +21,7 @@ These inputs apply when the action runs on `pull_request` events. For the comple | Input | Description | Default | |-------|-------------|---------| | `ai-pull-request-description` | Enable AI-generated PR descriptions using the configured `planner` agent runtime | "true" | +| `ai-pull-request-description-mode` | `replace`, `append`, `preserve`, or `disabled`; controls automatic ownership and explicit `/copilot description` updates | `replace` | | `pull-requests-locale` | Target locale for PR review comment translation | "en-US" | | `ai-members-only` | Restrict AI PR description to org/project members only | "false" | diff --git a/docs/pull-requests/workflow-setup.mdx b/docs/pull-requests/workflow-setup.mdx index 1703fd67..600935c6 100644 --- a/docs/pull-requests/workflow-setup.mdx +++ b/docs/pull-requests/workflow-setup.mdx @@ -15,6 +15,12 @@ Use the `pull_request` trigger with the types you need. Common setup: on: pull_request: types: [opened, reopened, edited, labeled, unlabeled, closed, assigned, unassigned, synchronize] + pull_request_review: + types: [submitted, edited, dismissed] + check_suite: + types: [completed, rerequested] + workflow_run: + types: [completed] ``` | Event type | When it runs | Typical use | @@ -76,6 +82,8 @@ jobs: 8. **Bugbot review:** On `opened`, `reopened`, `edited`, and `synchronize`, the reviewer role analyzes the PR head and publishes stable finding comments. Active findings move the PR to `state:changes-requested` and `state:awaiting-issue-author`; a clean review moves it to `state:ready` and `state:awaiting-maintainer`. During the run, `state:ai-processing` may coexist with those labels and is removed when the agent finishes. Configure `reviewer-provider`, `reviewer-model-provider`, `reviewer-model`, `reviewer-effort`, and `reviewer-command` only when the reviewer should differ from the base agent. +9. **External lifecycle evidence:** `pull_request_review` and check/workflow completion events update only the managed lifecycle label. A failed check blocks the PR, a pending check keeps it reviewing, an approved review plus successful/unknown checks marks it ready, and a requested change marks it as changes requested. These events do not invoke an agent pipeline. + ## Next steps - **[Capabilities](/pull-requests/capabilities)** — Detailed list of what the action does on PRs. diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index 8d62cd96..40382760 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -202,12 +202,25 @@ ai: bugbotSeverity: medium bugbotCommentLimit: 10 ignoreFiles: node_modules/*,build/*,dist/* + pullRequestDescriptionMode: append createInitialTag: true manageRepositoryVariables: true ``` Run it with `copilot setup --config .copilot-setup.yml`. The wizard rejects values that look like tokens, API keys, passwords, or other credential material. Secret values are accepted only through the hidden prompt or explicit CLI inputs and are written directly to GitHub Secrets after validation; they are never written to repository files or Variables. The required secret names are shown in the final plan; normally they include `PAT` plus the credentials needed by the selected runtime/model providers. +### `copilot reconcile` + +Checks whether the setup-managed workflow files in the current repository match the installed Copilot contract. It is read-only by default and does not call GitHub or change remote configuration. Use `--apply` to update only the detected setup-managed workflow files; those updates go through the same local backup/reconciliation path as setup. + +```bash +copilot reconcile +copilot reconcile --json +copilot reconcile --apply +``` + +The first version deliberately scopes reconciliation to workflow templates. Branches, labels, Repository Variables, and Secrets remain under `copilot setup`/`copilot doctor` until their remote diff and approval contracts are added. + ### `copilot check-progress` Checks the progress of an issue from the selected branch and updates the progress label on the issue and its related open pull requests. diff --git a/setup/workflows/copilot_commit.yml b/setup/workflows/copilot_commit.yml index 21e3df14..06c08880 100644 --- a/setup/workflows/copilot_commit.yml +++ b/setup/workflows/copilot_commit.yml @@ -37,6 +37,7 @@ jobs: pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-pull-request-description-mode: ${{ vars.AI_PULL_REQUEST_DESCRIPTION_MODE || 'replace' }} ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} diff --git a/setup/workflows/copilot_issue.yml b/setup/workflows/copilot_issue.yml index 871a3623..a5244edf 100644 --- a/setup/workflows/copilot_issue.yml +++ b/setup/workflows/copilot_issue.yml @@ -36,6 +36,7 @@ jobs: pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-pull-request-description-mode: ${{ vars.AI_PULL_REQUEST_DESCRIPTION_MODE || 'replace' }} ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} diff --git a/setup/workflows/copilot_issue_comment.yml b/setup/workflows/copilot_issue_comment.yml index 7f82eccc..300eaa96 100644 --- a/setup/workflows/copilot_issue_comment.yml +++ b/setup/workflows/copilot_issue_comment.yml @@ -36,6 +36,7 @@ jobs: pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-pull-request-description-mode: ${{ vars.AI_PULL_REQUEST_DESCRIPTION_MODE || 'replace' }} ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} diff --git a/setup/workflows/copilot_pull_request.yml b/setup/workflows/copilot_pull_request.yml index f93aa0ae..8830ac43 100644 --- a/setup/workflows/copilot_pull_request.yml +++ b/setup/workflows/copilot_pull_request.yml @@ -3,6 +3,12 @@ name: Copilot - Pull Request on: pull_request: types: [opened, reopened, edited, labeled, unlabeled, closed, assigned, unassigned, synchronize] + pull_request_review: + types: [submitted, edited, dismissed] + check_suite: + types: [completed, rerequested] + workflow_run: + types: [completed] jobs: copilot-pull-requests: @@ -34,6 +40,7 @@ jobs: pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-pull-request-description-mode: ${{ vars.AI_PULL_REQUEST_DESCRIPTION_MODE || 'replace' }} ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} diff --git a/setup/workflows/copilot_pull_request_comment.yml b/setup/workflows/copilot_pull_request_comment.yml index d8d8d02b..7a5534a0 100644 --- a/setup/workflows/copilot_pull_request_comment.yml +++ b/setup/workflows/copilot_pull_request_comment.yml @@ -36,6 +36,7 @@ jobs: pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} commit-prefix-transforms: ${{ vars.COMMIT_PREFIX_TRANSFORMS || 'replace-slash' }} ai-pull-request-description: ${{ vars.AI_PULL_REQUEST_DESCRIPTION || 'true' }} + ai-pull-request-description-mode: ${{ vars.AI_PULL_REQUEST_DESCRIPTION_MODE || 'replace' }} ai-members-only: ${{ vars.AI_MEMBERS_ONLY || 'false' }} ai-include-reasoning: ${{ vars.AI_INCLUDE_REASONING || 'true' }} bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} diff --git a/src/actions/github_action_ai_inputs.ts b/src/actions/github_action_ai_inputs.ts index 511044d0..71a8d3a8 100644 --- a/src/actions/github_action_ai_inputs.ts +++ b/src/actions/github_action_ai_inputs.ts @@ -4,10 +4,12 @@ import { parseBoundedPositiveIntegerInput } from './input_number_policy'; import { parseDelimitedValues } from './input_values_policy'; import { buildAgentTasksFromInputs } from './agent_input_builder'; import type { AgentTaskConfiguration } from '../data/model/agent'; +import { normalizePullRequestDescriptionMode, type PullRequestDescriptionMode } from '../domain/pull_request_description'; export interface GithubActionAiInputs { readonly requestedAgentTasks: AgentTaskConfiguration; readonly pullRequestDescription: boolean; + readonly pullRequestDescriptionMode: PullRequestDescriptionMode; readonly membersOnly: boolean; readonly includeReasoning: boolean; readonly ignoreFiles: string[]; @@ -25,6 +27,7 @@ export function readGithubActionAgentTasks( export function readGithubActionAiInputs(getInput: (key: string) => string): GithubActionAiInputs { const requestedAgentTasks = buildAgentTasksFromInputs(getInput); + const pullRequestDescription = isEnabledInput(getInput(INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); const verifyCommands = getInput(INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) .split(',') .map((command) => command.trim()) @@ -32,7 +35,10 @@ export function readGithubActionAiInputs(getInput: (key: string) => string): Git return { requestedAgentTasks, - pullRequestDescription: isEnabledInput(getInput(INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)), + pullRequestDescription, + pullRequestDescriptionMode: pullRequestDescription + ? normalizePullRequestDescriptionMode(getInput(INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) + : 'disabled', membersOnly: isEnabledInput(getInput(INPUT_KEYS.AI_MEMBERS_ONLY)), includeReasoning: isEnabledInput(getInput(INPUT_KEYS.AI_INCLUDE_REASONING)), ignoreFiles: parseDelimitedValues(getInput(INPUT_KEYS.AI_IGNORE_FILES)), diff --git a/src/actions/github_action_completion.ts b/src/actions/github_action_completion.ts index f51bcbab..6a00289e 100644 --- a/src/actions/github_action_completion.ts +++ b/src/actions/github_action_completion.ts @@ -57,6 +57,7 @@ async function writeActionSummary(execution: Execution, summaryPort?: ActionSumm : execution.labels?.currentIssueLabels ?? [], execution.labels?.lifecycle, ), + pullRequestDescriptionMode: execution.ai?.getPullRequestDescriptionMode?.(), results: execution.currentConfiguration.results, }); if (!summaryPort) return summaryText; diff --git a/src/actions/github_action_execution.ts b/src/actions/github_action_execution.ts index 82ba0b61..62f143de 100644 --- a/src/actions/github_action_execution.ts +++ b/src/actions/github_action_execution.ts @@ -91,6 +91,7 @@ export async function buildGithubActionExecution( aiInputs.bugbotCommentLimit, aiInputs.bugbotFixVerifyCommands, aiInputs.requestedAgentTasks, + aiInputs.pullRequestDescriptionMode, ), labels: buildLabels(labelInputs), issueTypes: buildIssueTypes(issueTypeInputs), diff --git a/src/actions/local_action_configuration_sections.ts b/src/actions/local_action_configuration_sections.ts index 122fc047..c699df43 100644 --- a/src/actions/local_action_configuration_sections.ts +++ b/src/actions/local_action_configuration_sections.ts @@ -10,6 +10,7 @@ import { parseBoundedPositiveIntegerInput, parseIntegerInput, parseNonNegativeIn import { parseDelimitedValues } from './input_values_policy'; import { buildAgentTasksFromValues } from './agent_input_builder'; import { buildImageConfiguration } from './image_configuration_builder'; +import { normalizePullRequestDescriptionMode } from '../domain/pull_request_description'; export type LocalActionInputs = ReturnType; @@ -41,10 +42,14 @@ export function readLocalAgentConfiguration( ) { const agentTasks = buildAgentTasksFromValues({ ...actionInputs, ...additionalParams }); const bugbotFixVerifyCommandsInput = input(additionalParams, actionInputs, INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) ?? ''; + const pullRequestDescription = isEnabledInput(input(additionalParams, actionInputs, INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); return { agentTasks, agentModel: agentTasks.findings.model, - aiPullRequestDescription: isEnabledInput(input(additionalParams, actionInputs, INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)), + aiPullRequestDescription: pullRequestDescription, + aiPullRequestDescriptionMode: pullRequestDescription + ? normalizePullRequestDescriptionMode(input(additionalParams, actionInputs, INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) + : 'disabled', aiMembersOnly: isEnabledInput(input(additionalParams, actionInputs, INPUT_KEYS.AI_MEMBERS_ONLY)), aiIncludeReasoning: isEnabledInput(input(additionalParams, actionInputs, INPUT_KEYS.AI_INCLUDE_REASONING)), aiIgnoreFilesInput: input(additionalParams, actionInputs, INPUT_KEYS.AI_IGNORE_FILES), diff --git a/src/actions/local_action_execution.ts b/src/actions/local_action_execution.ts index e4737794..7154cff9 100644 --- a/src/actions/local_action_execution.ts +++ b/src/actions/local_action_execution.ts @@ -18,7 +18,7 @@ export function buildLocalActionExecution( commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, - aiPullRequestDescription, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, + aiPullRequestDescription, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks, branchManagementLauncherLabel, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, @@ -71,6 +71,7 @@ export function buildLocalActionExecution( bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks, + aiPullRequestDescriptionMode, ), labels: buildLabels({ branching: { launcher: branchManagementLauncherLabel }, diff --git a/src/application/errors/application_error.ts b/src/application/errors/application_error.ts new file mode 100644 index 00000000..96f44a13 --- /dev/null +++ b/src/application/errors/application_error.ts @@ -0,0 +1,39 @@ +export type ApplicationErrorKind = + | 'configuration' + | 'authorization' + | 'provider' + | 'agent' + | 'validation' + | 'workflow' + | 'unknown'; + +export interface ApplicationErrorOptions { + readonly retryable?: boolean; + readonly cause?: unknown; +} + +/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ +export class ApplicationError extends Error { + readonly kind: ApplicationErrorKind; + readonly retryable: boolean; + readonly cause?: unknown; + + constructor(message: string, kind: ApplicationErrorKind = 'unknown', options: ApplicationErrorOptions = {}) { + super(message); + this.name = 'ApplicationError'; + this.kind = kind; + this.retryable = options.retryable ?? false; + this.cause = options.cause; + } +} + +export function toApplicationError( + error: unknown, + message: string, + kind: ApplicationErrorKind = 'unknown', + options: ApplicationErrorOptions = {}, +): ApplicationError { + return error instanceof ApplicationError + ? error + : new ApplicationError(message, kind, { ...options, cause: error }); +} diff --git a/src/application/policies/__tests__/action_summary_policy.test.ts b/src/application/policies/__tests__/action_summary_policy.test.ts index 96a2fdf7..fd66d36c 100644 --- a/src/application/policies/__tests__/action_summary_policy.test.ts +++ b/src/application/policies/__tests__/action_summary_policy.test.ts @@ -41,6 +41,7 @@ describe('action summary policy', () => { eventName: 'pull_request', issueNumber: -1, pullRequestNumber: 12, + pullRequestDescriptionMode: 'append', results: [new Result({ id: 'Review', success: true, @@ -51,5 +52,6 @@ describe('action summary policy', () => { expect(summary).toContain('❌ Failure'); expect(summary).toContain('open=1'); + expect(summary).toContain('append'); }); }); diff --git a/src/application/policies/__tests__/lifecycle_event_replay.test.ts b/src/application/policies/__tests__/lifecycle_event_replay.test.ts new file mode 100644 index 00000000..931e5889 --- /dev/null +++ b/src/application/policies/__tests__/lifecycle_event_replay.test.ts @@ -0,0 +1,37 @@ +import { PullRequest } from '../../../data/model/pull_request'; +import { readLifecycleExternalEvidence, resolveLifecycleState } from '../lifecycle_state_policy'; + +const baseDecision = { + isIssue: false, + isPullRequest: true, + issueOpened: false, + issueDescriptionEdited: false, + pullRequestMerged: false, + pullRequestClosed: false, + results: [], +}; + +describe('lifecycle event replay', () => { + it.each([ + ['pull_request_review', { action: 'submitted', review: { state: 'approved' }, pull_request: { number: 8 } }, 'ready'], + ['pull_request_review', { action: 'submitted', review: { state: 'changes_requested' }, pull_request: { number: 8 } }, 'changes-requested'], + ['check_suite', { action: 'completed', check_suite: { status: 'completed', conclusion: 'failure', pull_requests: [{ number: 8 }] } }, 'blocked'], + ['check_suite', { action: 'completed', check_suite: { status: 'queued', conclusion: null, pull_requests: [{ number: 8 }] } }, 'reviewing'], + ['workflow_run', { action: 'completed', workflow_run: { status: 'completed', conclusion: 'success', pull_requests: [{ number: 8 }] } }, 'reviewing'], + ])('replays %s into the expected lifecycle state', (eventName, payload, expectedState) => { + const pullRequest = new PullRequest(1, 1, 600, { + eventName, + ...payload, + }); + const evidence = readLifecycleExternalEvidence({ eventName, ...payload }); + + expect(pullRequest.isPullRequest).toBe(true); + expect(pullRequest.number).toBe(8); + expect(resolveLifecycleState({ + ...baseDecision, + eventName, + action: String(payload.action), + externalEvidence: evidence, + })).toBe(expectedState); + }); +}); diff --git a/src/application/policies/__tests__/lifecycle_state_policy.test.ts b/src/application/policies/__tests__/lifecycle_state_policy.test.ts index dd321c8a..632e01ca 100644 --- a/src/application/policies/__tests__/lifecycle_state_policy.test.ts +++ b/src/application/policies/__tests__/lifecycle_state_policy.test.ts @@ -1,4 +1,4 @@ -import { resolveLifecycleState } from '../lifecycle_state_policy'; +import { readLifecycleExternalEvidence, resolveLifecycleState } from '../lifecycle_state_policy'; const result = (id: string, success = true) => ({ id, success, executed: true, steps: [], errors: [] }); @@ -44,6 +44,39 @@ describe('lifecycle state policy', () => { })).toBe('ready'); }); + it('uses external review and check evidence without changing the legacy fallback', () => { + const base = { + eventName: 'pull_request_review', + action: 'submitted', + isIssue: false, + isPullRequest: true, + issueOpened: false, + issueDescriptionEdited: false, + pullRequestMerged: false, + pullRequestClosed: false, + results: [], + }; + expect(resolveLifecycleState({ ...base, externalEvidence: { review: 'changes-requested' } })).toBe('changes-requested'); + expect(resolveLifecycleState({ ...base, externalEvidence: { review: 'approved', checks: 'pending' } })).toBe('reviewing'); + expect(resolveLifecycleState({ ...base, externalEvidence: { review: 'approved', checks: 'success' } })).toBe('ready'); + expect(resolveLifecycleState({ ...base, externalEvidence: { checks: 'failure' } })).toBe('blocked'); + }); + + it('normalizes GitHub review and check payloads into stable evidence', () => { + expect(readLifecycleExternalEvidence({ + eventName: 'pull_request_review', + review: { state: 'changes_requested' }, + })).toEqual({ review: 'changes-requested' }); + expect(readLifecycleExternalEvidence({ + eventName: 'check_suite', + check_suite: { status: 'completed', conclusion: 'success' }, + })).toEqual({ checks: 'success' }); + expect(readLifecycleExternalEvidence({ + eventName: 'workflow_run', + workflow_run: { status: 'in_progress', conclusion: null }, + })).toEqual({ checks: 'pending' }); + }); + it('moves an explicit planning command on an issue to planned', () => { expect(resolveLifecycleState({ eventName: 'issue_comment', diff --git a/src/application/policies/__tests__/setup_configuration_policy.test.ts b/src/application/policies/__tests__/setup_configuration_policy.test.ts index e644c170..aa060d38 100644 --- a/src/application/policies/__tests__/setup_configuration_policy.test.ts +++ b/src/application/policies/__tests__/setup_configuration_policy.test.ts @@ -66,6 +66,7 @@ describe('setup configuration policy', () => { expect(buildSetupActionInputs(configuration)).toMatchObject({ 'planner-provider': 'cursor', 'reviewer-model': 'claude-3-7-sonnet', + 'ai-pull-request-description-mode': 'replace', }); expect(buildSetupPlan(configuration).warnings).toEqual(expect.arrayContaining([ expect.stringContaining('Cursor is an experimental runtime'), @@ -104,4 +105,16 @@ describe('setup configuration policy', () => { 'Model provider and model for planner cannot contain whitespace.', ])); }); + + it('validates and persists the PR description policy', () => { + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + ai: { pullRequestDescriptionMode: 'append' }, + }); + + expect(validateSetupConfiguration(configuration)).toEqual([]); + expect(buildSetupRepositoryVariables(configuration)).toEqual(expect.arrayContaining([ + { name: 'AI_PULL_REQUEST_DESCRIPTION_MODE', value: 'append' }, + ])); + expect(buildSetupActionInputs(configuration)['ai-pull-request-description-mode']).toBe('append'); + }); }); diff --git a/src/application/policies/__tests__/status_command_policy.test.ts b/src/application/policies/__tests__/status_command_policy.test.ts new file mode 100644 index 00000000..739b784b --- /dev/null +++ b/src/application/policies/__tests__/status_command_policy.test.ts @@ -0,0 +1,58 @@ +import { buildCopilotStatusResult, buildCopilotStatusSnapshot, formatCopilotStatus } from '../status_command_policy'; + +function execution(overrides: Record = {}) { + return { + owner: 'acme', + repo: 'demo', + eventName: 'pull_request', + isPush: false, + isIssue: false, + isPullRequest: true, + issue: { number: 17 }, + pullRequest: { number: 21, isPullRequestReviewComment: false }, + commit: { branch: 'feature/17-demo' }, + inputs: { action: 'synchronize' }, + labels: { + currentIssueLabels: ['state:in-progress'], + currentPullRequestLabels: ['size:m', 'state:reviewing'], + lifecycle: { + planned: 'state:planned', + inProgress: 'state:in-progress', + reviewing: 'state:reviewing', + changesRequested: 'state:changes-requested', + verified: 'state:verified', + ready: 'state:ready', + blocked: 'state:blocked', + awaitingMaintainer: 'state:awaiting-maintainer', + awaitingIssueAuthor: 'state:awaiting-issue-author', + }, + }, + currentConfiguration: { results: [] }, + ai: { + getPullRequestDescriptionMode: () => 'append', + getAiPullRequestDescription: () => true, + }, + ...overrides, + }; +} + +describe('status command policy', () => { + it('builds a read-only snapshot from setup facts', () => { + const snapshot = buildCopilotStatusSnapshot(execution() as never); + expect(snapshot).toMatchObject({ + target: 'pull-request', + issueNumber: 17, + pullRequestNumber: 21, + branch: 'feature/17-demo', + lifecycle: 'reviewing', + pullRequestDescriptionMode: 'append', + }); + }); + + it('renders a markdown status result without invoking an agent', () => { + const result = buildCopilotStatusResult(execution() as never, 'CommentAutomationUseCase'); + expect(result.success).toBe(true); + expect(result.executed).toBe(true); + expect(result.steps[0]).toContain('## Copilot status'); + }); +}); diff --git a/src/application/policies/action_summary_policy.ts b/src/application/policies/action_summary_policy.ts index af157417..8f85ff81 100644 --- a/src/application/policies/action_summary_policy.ts +++ b/src/application/policies/action_summary_policy.ts @@ -8,6 +8,7 @@ export interface ActionSummaryContext { readonly issueNumber: number; readonly pullRequestNumber: number; readonly lifecycleState?: string; + readonly pullRequestDescriptionMode?: string; readonly results: readonly Result[]; } @@ -30,6 +31,7 @@ export function buildActionSummary(context: ActionSummaryContext): string { `| Event | \`${escapeTable(context.eventName)}\` |`, `| Target | ${escapeTable(target)} |`, `| Lifecycle | ${lifecycle} |`, + `| PR description policy | ${escapeTable(context.pullRequestDescriptionMode ?? '—')} |`, `| Results | ${context.results.length} |`, `| Finding states | ${formatFindingStates(findingStates)} |`, ]; diff --git a/src/application/policies/agent_activity_policy.ts b/src/application/policies/agent_activity_policy.ts index f67db8f4..996c4634 100644 --- a/src/application/policies/agent_activity_policy.ts +++ b/src/application/policies/agent_activity_policy.ts @@ -58,7 +58,7 @@ function hasComment(execution: Execution): boolean { } function hasTarget(execution: Execution): boolean { - if (execution.eventName === 'pull_request' || execution.eventName === 'pull_request_review_comment') { + if (['pull_request', 'pull_request_review', 'pull_request_review_comment', 'check_suite', 'workflow_run'].includes(execution.eventName)) { return execution.pullRequest.number > 0; } return execution.issue.number > 0 || execution.issueNumber > 0; diff --git a/src/application/policies/lifecycle_state_policy.ts b/src/application/policies/lifecycle_state_policy.ts index 0160a5d3..d42cd867 100644 --- a/src/application/policies/lifecycle_state_policy.ts +++ b/src/application/policies/lifecycle_state_policy.ts @@ -1,5 +1,14 @@ import type { CopilotLifecycleState } from '../../domain/copilot_lifecycle'; import { getResultPayload } from '../../data/model/result'; +import type { ExecutionInputs } from '../../data/model/execution_inputs'; + +export type LifecycleChecksEvidence = 'pending' | 'success' | 'failure'; +export type LifecycleReviewEvidence = 'approved' | 'changes-requested' | 'commented' | 'dismissed'; + +export interface LifecycleExternalEvidence { + readonly checks?: LifecycleChecksEvidence; + readonly review?: LifecycleReviewEvidence; +} export interface LifecycleStatePolicyResult { readonly id: string; @@ -19,6 +28,7 @@ export interface LifecycleStateDecisionInput { readonly issueDescriptionEdited: boolean; readonly pullRequestMerged: boolean; readonly pullRequestClosed: boolean; + readonly externalEvidence?: LifecycleExternalEvidence; readonly results: readonly LifecycleStatePolicyResult[]; } @@ -31,11 +41,17 @@ export function resolveLifecycleState( if (input.isPullRequest) { if (input.pullRequestClosed && input.pullRequestMerged) return 'verified'; + if (input.externalEvidence?.checks === 'failure') return 'blocked'; + if (input.externalEvidence?.review === 'changes-requested') return 'changes-requested'; const findingState = input.results .map(result => getResultPayload(result.payload)?.findingStates) .find(isFindingStateCounts); if (findingState && (findingState.open > 0 || findingState.reopened > 0)) return 'changes-requested'; if (findingState && findingState.open === 0 && findingState.reopened === 0) return 'ready'; + if (input.externalEvidence?.checks === 'pending') return 'reviewing'; + if (input.externalEvidence?.review === 'approved') return 'ready'; + if (input.externalEvidence?.checks === 'success') return 'reviewing'; + if (input.externalEvidence?.review !== undefined) return 'reviewing'; if (['opened', 'reopened', 'synchronize'].includes(input.action)) return 'reviewing'; return undefined; } @@ -46,6 +62,31 @@ export function resolveLifecycleState( return undefined; } +/** Extracts only stable review/check facts from GitHub event payloads. */ +export function readLifecycleExternalEvidence(inputs: ExecutionInputs | undefined): LifecycleExternalEvidence | undefined { + if (!inputs) return undefined; + if (inputs.eventName === 'pull_request_review') { + const reviewState = inputs.review?.state?.trim().toLowerCase(); + if (reviewState === 'approved') return { review: 'approved' }; + if (reviewState === 'changes_requested') return { review: 'changes-requested' }; + if (reviewState === 'dismissed') return { review: 'dismissed' }; + if (reviewState === 'commented') return { review: 'commented' }; + return undefined; + } + if (inputs.eventName === 'check_suite') { + return { checks: readChecksEvidence(inputs.check_suite?.status, inputs.check_suite?.conclusion) }; + } + if (inputs.eventName === 'workflow_run') { + return { checks: readChecksEvidence(inputs.workflow_run?.status, inputs.workflow_run?.conclusion) }; + } + return undefined; +} + +function readChecksEvidence(status: string | undefined, conclusion: string | null | undefined): LifecycleChecksEvidence { + if (status?.trim().toLowerCase() !== 'completed') return 'pending'; + return conclusion?.trim().toLowerCase() === 'success' ? 'success' : 'failure'; +} + function isFindingStateCounts(value: unknown): value is { open: number; reopened: number } { return typeof value === 'object' && value !== null diff --git a/src/application/policies/lifecycle_waiting_state_policy.ts b/src/application/policies/lifecycle_waiting_state_policy.ts index 90f04b6a..fac9f37a 100644 --- a/src/application/policies/lifecycle_waiting_state_policy.ts +++ b/src/application/policies/lifecycle_waiting_state_policy.ts @@ -36,7 +36,10 @@ function isHumanInteraction(eventName: string): boolean { 'issues', 'issue_comment', 'pull_request', + 'pull_request_review', 'pull_request_review_comment', + 'check_suite', + 'workflow_run', 'push', ].includes(eventName); } diff --git a/src/application/policies/setup_configuration_policy.ts b/src/application/policies/setup_configuration_policy.ts index 30fba3cd..f8f2e9d7 100644 --- a/src/application/policies/setup_configuration_policy.ts +++ b/src/application/policies/setup_configuration_policy.ts @@ -14,6 +14,7 @@ import type { SetupCredentialRequirement, } from '../../domain/setup'; import { SUPPORTED_AGENT_PROVIDERS } from './agent_configuration_validation_policy'; +import { normalizePullRequestDescriptionMode } from '../../domain/pull_request_description'; export const SETUP_AGENT_TASKS: readonly AgentTask[] = [ 'planner', @@ -104,6 +105,7 @@ export function createDefaultSetupConfiguration(): SetupConfiguration { }, ai: { pullRequestDescription: true, + pullRequestDescriptionMode: 'replace', ignoreFiles: 'build/*', membersOnly: false, includeReasoning: true, @@ -188,6 +190,10 @@ export function validateSetupConfiguration(configuration: SetupConfiguration): s if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { errors.push('Bugbot severity must be info, low, medium, or high.'); } + if (configuration.ai.pullRequestDescriptionMode !== undefined + && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { + errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); + } if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } @@ -292,6 +298,7 @@ export function buildSetupRepositoryVariables(configuration: SetupConfiguration) add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); + add('AI_PULL_REQUEST_DESCRIPTION_MODE', configuration.ai.pullRequestDescriptionMode); add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); @@ -329,6 +336,7 @@ export function buildSetupActionInputs(configuration: SetupConfiguration): Recor 'pull-requests-locale': repository.pullRequestLocale, 'commit-prefix-transforms': repository.commitPrefixTransforms, 'ai-pull-request-description': String(ai.pullRequestDescription), + 'ai-pull-request-description-mode': normalizePullRequestDescriptionMode(ai.pullRequestDescriptionMode), 'ai-ignore-files': ai.ignoreFiles, 'ai-members-only': String(ai.membersOnly), 'ai-include-reasoning': String(ai.includeReasoning), diff --git a/src/application/policies/status_command_policy.ts b/src/application/policies/status_command_policy.ts new file mode 100644 index 00000000..862cfe4d --- /dev/null +++ b/src/application/policies/status_command_policy.ts @@ -0,0 +1,107 @@ +import type { Execution } from '../../data/model/execution'; +import { getResultPayload, Result } from '../../data/model/result'; + +export interface CopilotStatusSnapshot { + readonly owner: string; + readonly repository: string; + readonly event: string; + readonly action: string; + readonly target: 'issue' | 'pull-request' | 'push' | 'repository'; + readonly issueNumber?: number; + readonly pullRequestNumber?: number; + readonly branch?: string; + readonly lifecycle?: string; + readonly waitingFor?: string; + readonly issueLabels: readonly string[]; + readonly pullRequestLabels: readonly string[]; + readonly activeFindings?: { open: number; reopened: number; resolved: number }; + readonly pullRequestDescriptionMode: string; +} + +/** Builds a read-only status snapshot from the facts already loaded by setup. */ +export function buildCopilotStatusSnapshot(execution: Execution): CopilotStatusSnapshot { + const issueLabels = [...(execution.labels?.currentIssueLabels ?? [])]; + const pullRequestLabels = [...(execution.labels?.currentPullRequestLabels ?? [])]; + const isPullRequestTarget = execution.isPullRequest || execution.pullRequest?.number > 0 || execution.pullRequest?.isPullRequestReviewComment; + const targetLabels = isPullRequestTarget ? pullRequestLabels : issueLabels; + const lifecycleLabels = execution.labels?.lifecycle ?? {}; + const lifecycle = Object.entries({ + planned: lifecycleLabels.planned, + 'in-progress': lifecycleLabels.inProgress, + reviewing: lifecycleLabels.reviewing, + 'changes-requested': lifecycleLabels.changesRequested, + verified: lifecycleLabels.verified, + ready: lifecycleLabels.ready, + blocked: lifecycleLabels.blocked, + }).find(([, label]) => label && targetLabels.includes(label))?.[0]; + const waitingFor = Object.entries({ + maintainer: lifecycleLabels.awaitingMaintainer, + 'issue-author': lifecycleLabels.awaitingIssueAuthor, + }).find(([, label]) => label && targetLabels.includes(label))?.[0]; + const findingStates = execution.currentConfiguration?.results + ?.map(result => getResultPayload(result.payload)?.findingStates) + .find(isFindingStateCounts); + + return { + owner: execution.owner, + repository: execution.repo, + event: execution.eventName || 'unknown', + action: execution.inputs?.action ?? '', + target: execution.pullRequest?.number > 0 || execution.pullRequest?.isPullRequestReviewComment + ? 'pull-request' + : execution.isPush + ? 'push' + : execution.issue?.number > 0 || execution.isIssue + ? 'issue' + : 'repository', + ...(execution.issue?.number > 0 ? { issueNumber: execution.issue.number } : {}), + ...(execution.pullRequest?.number > 0 ? { pullRequestNumber: execution.pullRequest.number } : {}), + ...(execution.commit?.branch ? { branch: execution.commit.branch } : {}), + ...(lifecycle ? { lifecycle } : {}), + ...(waitingFor ? { waitingFor } : {}), + issueLabels, + pullRequestLabels, + ...(findingStates ? { activeFindings: findingStates } : {}), + pullRequestDescriptionMode: execution.ai.getPullRequestDescriptionMode?.() + ?? (execution.ai.getAiPullRequestDescription() ? 'replace' : 'disabled'), + }; +} + +export function buildCopilotStatusResult(execution: Execution, taskId: string): Result { + const snapshot = buildCopilotStatusSnapshot(execution); + return new Result({ + id: `${taskId}.Status`, + success: true, + executed: true, + stepFormat: 'markdown', + steps: [formatCopilotStatus(snapshot)], + payload: { status: snapshot }, + }); +} + +export function formatCopilotStatus(snapshot: CopilotStatusSnapshot): string { + const lines = [ + '## Copilot status', + `- **Repository:** ${snapshot.owner}/${snapshot.repository}`, + `- **Target:** ${snapshot.target}${snapshot.issueNumber ? ` #${snapshot.issueNumber}` : ''}${snapshot.pullRequestNumber ? ` / PR #${snapshot.pullRequestNumber}` : ''}`, + `- **Event:** ${snapshot.event}${snapshot.action ? ` (${snapshot.action})` : ''}`, + `- **Branch:** ${snapshot.branch ?? 'unknown'}`, + `- **Lifecycle:** ${snapshot.lifecycle ?? 'not set'}`, + `- **Waiting for:** ${snapshot.waitingFor ?? 'no pending human response'}`, + `- **PR description policy:** ${snapshot.pullRequestDescriptionMode}`, + `- **Issue labels:** ${snapshot.issueLabels.length > 0 ? snapshot.issueLabels.join(', ') : 'none'}`, + `- **PR labels:** ${snapshot.pullRequestLabels.length > 0 ? snapshot.pullRequestLabels.join(', ') : 'none'}`, + ]; + if (snapshot.activeFindings) { + lines.push(`- **Bugbot findings:** ${snapshot.activeFindings.open} open, ${snapshot.activeFindings.reopened} reopened, ${snapshot.activeFindings.resolved} resolved`); + } + return lines.join('\n'); +} + +function isFindingStateCounts(value: unknown): value is { open: number; reopened: number; resolved: number } { + return typeof value === 'object' + && value !== null + && typeof (value as { open?: unknown }).open === 'number' + && typeof (value as { reopened?: unknown }).reopened === 'number' + && typeof (value as { resolved?: unknown }).resolved === 'number'; +} diff --git a/src/application/ports/execution_resolution_ports.ts b/src/application/ports/execution_resolution_ports.ts index b7a059a9..a10ab374 100644 --- a/src/application/ports/execution_resolution_ports.ts +++ b/src/application/ports/execution_resolution_ports.ts @@ -11,7 +11,7 @@ export interface ExecutionIssueResolutionContext { repo: string; tokens: { token: string }; issue: { number: number }; - pullRequest: { head: string }; + pullRequest: { head: string; number?: number }; commit: { branch: string }; isSingleAction: boolean; isIssue: boolean; diff --git a/src/application/ports/pull_request_description_ports.ts b/src/application/ports/pull_request_description_ports.ts index 8b675524..6369a5cd 100644 --- a/src/application/ports/pull_request_description_ports.ts +++ b/src/application/ports/pull_request_description_ports.ts @@ -1,3 +1,11 @@ +export interface PullRequestDescriptionDetails { + readonly body: string; + readonly headBranch: string; + readonly baseBranch: string; +} + export interface PullRequestDescriptionCommandPort { updateDescription(owner: string, repository: string, pullRequestNumber: number, description: string, token: string): Promise; + /** Optional read capability used by explicit commands from issue comments. */ + getDetails?(owner: string, repository: string, pullRequestNumber: number, token: string): Promise; } diff --git a/src/application/usecases/__tests__/comment_automation_use_case.test.ts b/src/application/usecases/__tests__/comment_automation_use_case.test.ts index 095b6f25..f09ed87e 100644 --- a/src/application/usecases/__tests__/comment_automation_use_case.test.ts +++ b/src/application/usecases/__tests__/comment_automation_use_case.test.ts @@ -203,6 +203,34 @@ describe("runCommentAutomation", () => { expect(think.invoke).not.toHaveBeenCalled(); }); + it('routes explicit PR description commands without language or intent detection', async () => { + const description = { invokeExplicit: jest.fn().mockResolvedValue([successfulResult('description')]) }; + const language = { invoke: jest.fn() }; + const intent = { invoke: jest.fn() }; + const results = await runCommentAutomation( + { owner: 'o', repo: 'r', actor: 'actor', tokens: { token: 't' } } as Execution, + { + taskId: 'CommentAutomation', + languageUseCase: language as never, + intentUseCase: intent as never, + thinkUseCase: {} as never, + autofixUseCase: {} as never, + doUserRequestUseCase: {} as never, + userComment: '/copilot description', + gitCommitPort: {} as never, + updatePullRequestDescriptionUseCase: description, + }, + {} as never, + {} as never, + {} as never, + ); + + expect(results).toEqual([expect.objectContaining({ id: 'description' })]); + expect(description.invokeExplicit).toHaveBeenCalledTimes(1); + expect(language.invoke).not.toHaveBeenCalled(); + expect(intent.invoke).not.toHaveBeenCalled(); + }); + it('rejects an invalid explicit command without invoking an agent', async () => { const think = { invoke: jest.fn() }; const results = await runCommentAutomation( diff --git a/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts b/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts index 2fb6d4cd..56ebbde8 100644 --- a/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts @@ -151,4 +151,28 @@ describe('SynchronizeLifecycleStateUseCase', () => { 'token', ); }); + + it('synchronizes check-suite evidence for a PR without invoking the agent route', async () => { + const setLabels = jest.fn().mockResolvedValue(undefined); + const useCase = new SynchronizeLifecycleStateUseCase({ setLabels, getLabels: jest.fn() }); + const param = execution({ + eventName: 'check_suite', + inputs: { + eventName: 'check_suite', + action: 'completed', + check_suite: { status: 'completed', conclusion: 'failure', pull_requests: [{ number: 11 }] }, + }, + issue: { number: -1, opened: false, descriptionEdited: false }, + pullRequest: { number: 11, isMerged: false, isClosed: false }, + labels: { + ...execution().labels, + currentIssueLabels: [], + currentPullRequestLabels: ['state:reviewing'], + }, + }); + + await useCase.invoke({ execution: param, results: [] }); + + expect(setLabels).toHaveBeenCalledWith('owner', 'repo', 11, ['state:blocked', 'state:awaiting-maintainer'], 'token'); + }); }); diff --git a/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts b/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts index e9486c4a..ae416ea4 100644 --- a/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts +++ b/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts @@ -1,7 +1,7 @@ import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; import { lifecycleLabelNames, lifecycleStateLabel, waitingLabelNames, waitingStateLabel } from '../../../domain/copilot_lifecycle'; -import { resolveLifecycleState } from '../../policies/lifecycle_state_policy'; +import { readLifecycleExternalEvidence, resolveLifecycleState } from '../../policies/lifecycle_state_policy'; import { resolveLifecycleWaitingState, type LifecycleWaitingStateDecision } from '../../policies/lifecycle_waiting_state_policy'; import type { IssueLabelsPort } from '../../ports/issue_management_ports'; import { logDebugInfo, logError } from '../../ports/logging_ports'; @@ -11,6 +11,14 @@ export interface SynchronizeLifecycleStateParam { results: readonly Result[]; } +const PULL_REQUEST_LIFECYCLE_EVENTS = [ + 'pull_request', + 'pull_request_review', + 'pull_request_review_comment', + 'check_suite', + 'workflow_run', +]; + /** * Reconciles one state label after a route completes. The existing business * labels remain untouched, and repeated events are idempotent. @@ -25,11 +33,12 @@ export class SynchronizeLifecycleStateUseCase { eventName: param.execution.eventName, action: param.execution.inputs?.action ?? '', isIssue: ['issues', 'issue_comment'].includes(param.execution.eventName), - isPullRequest: ['pull_request', 'pull_request_review_comment'].includes(param.execution.eventName), + isPullRequest: param.execution.isPullRequest || PULL_REQUEST_LIFECYCLE_EVENTS.includes(param.execution.eventName), issueOpened: param.execution.issue.opened, issueDescriptionEdited: param.execution.issue.descriptionEdited, pullRequestMerged: param.execution.pullRequest.isMerged, pullRequestClosed: param.execution.pullRequest.isClosed, + externalEvidence: readLifecycleExternalEvidence(param.execution.inputs), results: param.results, }); const waitingDecision = resolveLifecycleWaitingState({ @@ -87,18 +96,18 @@ function targetNumber(execution: Execution): number { if (['issues', 'issue_comment', 'push'].includes(execution.eventName)) { return execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; } - if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) return execution.pullRequest.number; + if (PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName)) return execution.pullRequest.number; return -1; } function targetLabels(execution: Execution): string[] { - return ['pull_request', 'pull_request_review_comment'].includes(execution.eventName) + return PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName) ? execution.labels.currentPullRequestLabels : execution.labels.currentIssueLabels; } function setTargetLabels(execution: Execution, labels: string[]): void { - if (['pull_request', 'pull_request_review_comment'].includes(execution.eventName)) { + if (PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName)) { execution.labels.currentPullRequestLabels = labels; } else execution.labels.currentIssueLabels = labels; diff --git a/src/application/usecases/comment_automation_command_workflow.ts b/src/application/usecases/comment_automation_command_workflow.ts index 54abc3f8..f2bd6e3e 100644 --- a/src/application/usecases/comment_automation_command_workflow.ts +++ b/src/application/usecases/comment_automation_command_workflow.ts @@ -3,6 +3,7 @@ import type { Execution } from '../../data/model/execution'; import type { ActorAuthorizationPort } from '../ports/actor_authorization_ports'; import type { CommentAutomationOptions } from './comment_automation_contracts'; import type { ParsedCopilotCommand } from '../../domain/copilot_command'; +import { buildCopilotStatusResult } from '../policies/status_command_policy'; /** Executes deterministic /copilot commands without routing them through intent detection. */ export async function runExplicitCommentCommand( @@ -11,12 +12,29 @@ export async function runExplicitCommentCommand( command: ParsedCopilotCommand, actorAuthorizationPort: ActorAuthorizationPort, ): Promise { + if (command.name === 'status') return [buildCopilotStatusResult(param, options.taskId)]; if (command.name === 'dismiss') return runDismissCommand(param, options, command, actorAuthorizationPort); + if (command.name === 'description') return runDescriptionCommand(param, options); if (['review', 'findings', 'recheck'].includes(command.name)) return runReviewCommand(param, options, command); if (command.name === 'fix') return undefined; return runThinkCommand(param, options, command); } +async function runDescriptionCommand( + param: Execution, + options: CommentAutomationOptions, +): Promise { + if (!options.updatePullRequestDescriptionUseCase) { + return [new Result({ + id: `${options.taskId}.Description`, + success: false, + executed: false, + errors: ['Explicit pull-request description command is not available in this composition.'], + })]; + } + return options.updatePullRequestDescriptionUseCase.invokeExplicit(param); +} + async function runDismissCommand( param: Execution, options: CommentAutomationOptions, diff --git a/src/application/usecases/comment_automation_contracts.ts b/src/application/usecases/comment_automation_contracts.ts index 6d4b377f..19e13089 100644 --- a/src/application/usecases/comment_automation_contracts.ts +++ b/src/application/usecases/comment_automation_contracts.ts @@ -6,6 +6,10 @@ import type { DoUserRequestParam } from "./steps/commit/user_request_use_case"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +export interface ExplicitPullRequestDescriptionUseCase { + invokeExplicit(param: Execution): Promise; +} + export interface CommentAutomationOptions { taskId: string; languageUseCase: ParamUseCase; @@ -18,4 +22,6 @@ export interface CommentAutomationOptions { userComment: string; gitCommitPort: GitCommitPort; dismissBugbotFindingsUseCase?: ParamUseCase; + /** Optional explicit PR description command; automatic PR updates remain a separate route. */ + updatePullRequestDescriptionUseCase?: ExplicitPullRequestDescriptionUseCase; } diff --git a/src/application/usecases/comment_automation_use_case.ts b/src/application/usecases/comment_automation_use_case.ts index ada87f3d..5e5876b3 100644 --- a/src/application/usecases/comment_automation_use_case.ts +++ b/src/application/usecases/comment_automation_use_case.ts @@ -8,16 +8,10 @@ import type { CommentAutomationOptions } from './comment_automation_contracts'; import { parseCopilotCommand } from '../../domain/copilot_command'; import { invalidCommentCommandResult, runExplicitCommentCommand } from './comment_automation_command_workflow'; import { runNaturalLanguageCommentAutomation } from './comment_automation_natural_language_workflow'; +import { ApplicationError } from '../errors/application_error'; export type { CommentAutomationOptions } from "./comment_automation_contracts"; -class CommentAutomationError extends Error { - constructor() { - super("Comment automation failed."); - this.name = "CommentAutomationError"; - } -} - export async function runCommentAutomation( param: Execution, options: CommentAutomationOptions, @@ -41,8 +35,8 @@ export async function runCommentAutomation( authenticatedUserPort, bugbotResolutionPorts, }); - } catch { - const error = new CommentAutomationError(); + } catch (cause) { + const error = new ApplicationError("Comment automation failed.", 'workflow', { cause }); logError(error); return [...languageResults, new Result({ id: options.taskId, diff --git a/src/application/usecases/execution/execution_issue_number_policy.ts b/src/application/usecases/execution/execution_issue_number_policy.ts index ce04fd20..350f1b39 100644 --- a/src/application/usecases/execution/execution_issue_number_policy.ts +++ b/src/application/usecases/execution/execution_issue_number_policy.ts @@ -8,7 +8,13 @@ type IssueRepository = Pick { taskId = "IssueCommentUseCase"; @@ -30,6 +31,7 @@ export class IssueCommentUseCase implements ParamUseCase { private readonly gitCommitPort: GitCommitPort, private readonly dismissBugbotFindingsUseCase?: ParamUseCase, private readonly reviewPotentialProblemsUseCase?: ParamUseCase, + private readonly updatePullRequestDescriptionUseCase?: UpdatePullRequestDescriptionUseCase, ) {} async invoke(param: Execution): Promise { @@ -46,6 +48,7 @@ export class IssueCommentUseCase implements ParamUseCase { gitCommitPort: this.gitCommitPort, dismissBugbotFindingsUseCase: this.dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase: this.reviewPotentialProblemsUseCase, + updatePullRequestDescriptionUseCase: this.updatePullRequestDescriptionUseCase, }, this.actorAuthorizationPort, this.authenticatedUserPort, diff --git a/src/application/usecases/pull_request_review_comment_use_case.ts b/src/application/usecases/pull_request_review_comment_use_case.ts index 79331a23..d4cfa336 100644 --- a/src/application/usecases/pull_request_review_comment_use_case.ts +++ b/src/application/usecases/pull_request_review_comment_use_case.ts @@ -10,6 +10,7 @@ import type { ActorAuthorizationPort } from "../ports/actor_authorization_ports" import type { BugbotFindingResolutionPorts } from "../ports/bugbot_finding_resolution_ports"; import type { GitCommitPort } from "../ports/git_ports"; import type { DismissBugbotFindingsParam } from './steps/commit/bugbot/dismiss_bugbot_findings_use_case'; +import type { UpdatePullRequestDescriptionUseCase } from './steps/pull_request/update_pull_request_description_use_case'; export class PullRequestReviewCommentUseCase implements ParamUseCase< Execution, @@ -33,6 +34,7 @@ export class PullRequestReviewCommentUseCase implements ParamUseCase< private readonly gitCommitPort: GitCommitPort, private readonly dismissBugbotFindingsUseCase?: ParamUseCase, private readonly reviewPotentialProblemsUseCase?: ParamUseCase, + private readonly updatePullRequestDescriptionUseCase?: UpdatePullRequestDescriptionUseCase, ) {} async invoke(param: Execution): Promise { @@ -49,6 +51,7 @@ export class PullRequestReviewCommentUseCase implements ParamUseCase< gitCommitPort: this.gitCommitPort, dismissBugbotFindingsUseCase: this.dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase: this.reviewPotentialProblemsUseCase, + updatePullRequestDescriptionUseCase: this.updatePullRequestDescriptionUseCase, }, this.actorAuthorizationPort, this.authenticatedUserPort, diff --git a/src/application/usecases/pull_request_workflow.ts b/src/application/usecases/pull_request_workflow.ts index 31a46818..420c55b1 100644 --- a/src/application/usecases/pull_request_workflow.ts +++ b/src/application/usecases/pull_request_workflow.ts @@ -3,6 +3,7 @@ import { Result } from "../../data/model/result"; import { logDebugInfo, logError } from "../ports/logging_ports"; import type { ParamUseCase } from "./base/param_usecase"; import type { PullRequestWorkflowSteps } from "./pull_request_workflow_steps"; +import { ApplicationError } from '../errors/application_error'; export interface PullRequestWorkflowPorts { updatePullRequestDescriptionUseCase: ParamUseCase; @@ -29,7 +30,7 @@ export async function runPullRequestWorkflow( ports.workflowSteps.checkPriorityPullRequestSize, ]; const results = await runSteps(param, steps); - if (param.ai.getAiPullRequestDescription()) { + if (shouldUpdatePullRequestDescriptionAutomatically(param)) { results.push(...(await ports.updatePullRequestDescriptionUseCase.invoke(param))); } results.push(...(await runPullRequestReview(param, ports))); @@ -37,7 +38,7 @@ export async function runPullRequestWorkflow( } if (param.pullRequest.isSynchronize) { - const results = param.ai.getAiPullRequestDescription() + const results = shouldUpdatePullRequestDescriptionAutomatically(param) ? await ports.updatePullRequestDescriptionUseCase.invoke(param) : []; results.push(...(await runPullRequestReview(param, ports))); @@ -47,8 +48,8 @@ export async function runPullRequestWorkflow( if (param.pullRequest.isClosed && param.pullRequest.isMerged) { return ports.workflowSteps.closeIssueAfterMerging.invoke(param); } - } catch { - const semanticError = new Error("Unable to process the pull request."); + } catch (cause) { + const semanticError = new ApplicationError("Unable to process the pull request.", 'workflow', { cause }); logError(semanticError); return [ new Result({ @@ -63,6 +64,13 @@ export async function runPullRequestWorkflow( return []; } +function shouldUpdatePullRequestDescriptionAutomatically(param: Execution): boolean { + const mode = param.ai.getPullRequestDescriptionMode?.(); + return mode === undefined + ? param.ai.getAiPullRequestDescription() + : mode === 'replace' || mode === 'append'; +} + async function runPullRequestReview( param: Execution, ports: PullRequestWorkflowPorts, diff --git a/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.ts b/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.ts index ed7c0fdf..6b20cb35 100644 --- a/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.ts +++ b/src/application/usecases/steps/pull_request/update_pull_request_description_use_case.ts @@ -26,4 +26,14 @@ export class UpdatePullRequestDescriptionUseCase implements ParamUseCase { + return await runUpdatePullRequestDescriptionWorkflow(param, this.taskId, { + pullRequestDescriptionCommandPort: this.pullRequestDescriptionCommandPort, + issueDescriptionQueryPort: this.issueDescriptionQueryPort, + organizationMembersPort: this.organizationMembersPort, + aiRepository: this.aiRepository, + }, true); + } } diff --git a/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.ts b/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.ts index 3b732ae0..183492ca 100644 --- a/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.ts +++ b/src/application/usecases/steps/pull_request/update_pull_request_description_workflow.ts @@ -10,6 +10,12 @@ import { logDebugInfo, logError, logInfo } from '../../../ports/logging_ports'; import { PROJECT_CONTEXT_INSTRUCTION } from '../../../../utils/project_context_instruction'; import { getTaskEmoji } from '../../../../utils/task_emoji'; import { sanitizeAgentMarkdown } from '../../../../application/policies/github_comment_publication_policy'; +import { + mergeManagedPullRequestDescription, + shouldAutomaticallyUpdatePullRequestDescription, + type PullRequestDescriptionMode, +} from '../../../../domain/pull_request_description'; +import { ApplicationError } from '../../../errors/application_error'; export interface UpdatePullRequestDescriptionWorkflowDependencies { pullRequestDescriptionCommandPort: PullRequestDescriptionCommandPort; @@ -23,11 +29,14 @@ export async function runUpdatePullRequestDescriptionWorkflow( param: Execution, taskId: string, dependencies: UpdatePullRequestDescriptionWorkflowDependencies, + force = false, ): Promise { logInfo(`${getTaskEmoji(taskId)} Executing ${taskId} (AI PR description).`); try { - const branches = getPullRequestBranches(param); + const pullRequestNumber = getPullRequestNumber(param); + const details = await loadPullRequestDetails(param, dependencies, pullRequestNumber, force); + const branches = getPullRequestBranches(param, details); if (!branches) { return [ new Result({ @@ -41,6 +50,11 @@ export async function runUpdatePullRequestDescriptionWorkflow( ]; } + const mode = getPullRequestDescriptionMode(param); + if (mode === 'disabled' || (!force && !shouldAutomaticallyUpdatePullRequestDescription(mode))) { + return skipped(taskId, `Automatic PR description updates are disabled by the "${mode}" mode.`); + } + logDebugInfo( `PR description will be generated from workspace diff: base "${branches.baseBranch}", head "${branches.headBranch}" (configured agent will run git diff).`, ); @@ -87,7 +101,10 @@ export async function runUpdatePullRequestDescriptionWorkflow( agentId: AGENT_PLAN, prompt, }); - const pullRequestBody = sanitizeAgentMarkdown(extractDescription(response)); + const generatedDescription = sanitizeAgentMarkdown(extractDescription(response)); + const pullRequestBody = mode === 'replace' + ? generatedDescription + : mergeManagedPullRequestDescription(details?.body ?? param.pullRequest.body, generatedDescription); logDebugInfo(`UpdatePullRequestDescription: agent response received. Description length=${pullRequestBody.length}.`); if (!pullRequestBody.trim()) { return newResult(taskId, false, true, ['Configured agent did not return a PR description.']); @@ -96,30 +113,64 @@ export async function runUpdatePullRequestDescriptionWorkflow( await dependencies.pullRequestDescriptionCommandPort.updateDescription( param.owner, param.repo, - param.pullRequest.number, + pullRequestNumber, pullRequestBody, param.tokens.token, ); return [new Result({ id: taskId, success: true, executed: true, steps: [] })]; - } catch (error) { + } catch (cause) { + const error = new ApplicationError('Unable to update pull request description.', 'workflow', { cause }); logError(error); return [ new Result({ id: taskId, success: false, executed: true, - steps: [`Error updating pull request description: ${error}`], + steps: [error.message], + errors: [error], }), ]; } } -function getPullRequestBranches(param: Execution): { headBranch: string; baseBranch: string } | undefined { - const headBranch = param.pullRequest.head; - const baseBranch = param.pullRequest.base; +function getPullRequestBranches( + param: Execution, + details?: { headBranch: string; baseBranch: string }, +): { headBranch: string; baseBranch: string } | undefined { + const headBranch = param.pullRequest.head || details?.headBranch; + const baseBranch = param.pullRequest.base || details?.baseBranch; return headBranch && baseBranch ? { headBranch, baseBranch } : undefined; } +function getPullRequestNumber(param: Execution): number { + return param.pullRequest.number > 0 ? param.pullRequest.number : param.issue.number; +} + +async function loadPullRequestDetails( + param: Execution, + dependencies: UpdatePullRequestDescriptionWorkflowDependencies, + pullRequestNumber: number, + force: boolean, +): Promise<{ body: string; headBranch: string; baseBranch: string } | undefined> { + if (pullRequestNumber <= 0 || !dependencies.pullRequestDescriptionCommandPort.getDetails) return undefined; + const needsRemoteDetails = param.eventName === 'issue_comment' + || force + || !param.pullRequest.head + || !param.pullRequest.base; + if (!needsRemoteDetails) return undefined; + return dependencies.pullRequestDescriptionCommandPort.getDetails( + param.owner, + param.repo, + pullRequestNumber, + param.tokens.token, + ); +} + +function getPullRequestDescriptionMode(param: Execution): PullRequestDescriptionMode { + return param.ai.getPullRequestDescriptionMode?.() + ?? (param.ai.getAiPullRequestDescription() ? 'replace' : 'disabled'); +} + function extractDescription(response: string | Record | undefined): string { if (typeof response === 'string') return response; if (!response) return ''; diff --git a/src/cli/command_registry.ts b/src/cli/command_registry.ts index c0c07e0d..77a46413 100644 --- a/src/cli/command_registry.ts +++ b/src/cli/command_registry.ts @@ -7,6 +7,7 @@ import { registerDetectPotentialProblemsCommand } from './commands/detect_potent import { registerSetupCommand } from './commands/setup'; import { registerUpgradeCommand } from './commands/upgrade'; import { registerDoctorCommand } from './commands/doctor'; +import { registerReconcileCommand } from './commands/reconcile'; export function registerCliCommands(program: Command): Command { registerThinkCommand(program); @@ -17,5 +18,6 @@ export function registerCliCommands(program: Command): Command { registerSetupCommand(program); registerUpgradeCommand(program); registerDoctorCommand(program); + registerReconcileCommand(program); return program; } diff --git a/src/cli/commands/reconcile.ts b/src/cli/commands/reconcile.ts new file mode 100644 index 00000000..05506c73 --- /dev/null +++ b/src/cli/commands/reconcile.ts @@ -0,0 +1,79 @@ +import { Command } from 'commander'; +import { getGitInfo, isInsideGitRepo } from '../../cli_context'; +import { createDefaultSetupConfiguration, mergeSetupConfiguration } from '../../application/policies/setup_configuration_policy'; +import { loadSetupConfigurationOverrides } from '../setup_config_file'; +import { SetupWorkspaceAdapter } from '../../infrastructure/setup_workspace_adapter'; +import type { SetupWorkspacePort, SetupWorkspaceResult } from '../../application/ports/setup_workspace_ports'; + +export interface ReconcileCommandOptions { + config?: string; + apply?: boolean; + json?: boolean; +} + +/** Reconciles setup-managed workflow files locally; remote GitHub state is never changed. */ +export function registerReconcileCommand(program: Command): void { + program + .command('reconcile') + .description('Detect setup drift and optionally reconcile setup-managed workflow files') + .option('--config ', 'YAML or JSON setup configuration used as the expected contract') + .option('--apply', 'Apply local workflow/template reconciliation after showing the drift') + .option('--json', 'Print a machine-readable reconciliation report') + .action((options: ReconcileCommandOptions) => runReconcileCommand(options)); +} + +export function runReconcileCommand( + options: ReconcileCommandOptions, + workspace: SetupWorkspacePort = new SetupWorkspaceAdapter(), +): void { + const cwd = process.cwd(); + if (!isInsideGitRepo(cwd)) throw new Error('Run "copilot reconcile" from the root of a git repository.'); + const gitInfo = getGitInfo(); + if ('error' in gitInfo) throw new Error(gitInfo.error); + + const overrides = options.config ? loadSetupConfigurationOverrides(options.config) : {}; + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), overrides); + const comparisons = [...(workspace.compareWorkflows?.(configuration.features) ?? [])]; + const drift = comparisons.filter(comparison => comparison.status !== 'unchanged'); + const report = { + repository: `${gitInfo.owner}/${gitInfo.repo}`, + scope: 'setup-workflows', + driftDetected: drift.length > 0, + applied: false, + files: comparisons, + result: undefined as SetupWorkspaceResult | undefined, + }; + + if (options.apply && drift.length > 0) { + report.result = workspace.prepare({ + features: configuration.features, + updateExistingWorkflows: true, + approvedWorkflowFiles: drift.map(comparison => comparison.file), + }); + report.applied = true; + } + + if (options.json) { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(`🔎 Reconciling ${report.scope} for ${report.repository}...`); + if (comparisons.length === 0) console.log(' No setup-managed workflows were found in the package contract.'); + for (const comparison of comparisons) { + const icon = comparison.status === 'unchanged' ? '✅' : comparison.status === 'missing' ? '❌' : '⚠️'; + console.log(` ${icon} ${comparison.destination} (${comparison.status})`); + } + if (report.result) console.log(`✅ Reconciliation applied: ${report.result.copied} copied, ${report.result.skipped} skipped.`); + } + + if (report.applied) { + process.exitCode = 0; + return; + } + + if (drift.length > 0) { + if (!options.json) console.log('ℹ️ Run with --apply to reconcile the local setup-managed files.'); + process.exitCode = 1; + } else { + process.exitCode = 0; + } +} diff --git a/src/cli/setup_config_file.ts b/src/cli/setup_config_file.ts index 567b422e..c914aa0e 100644 --- a/src/cli/setup_config_file.ts +++ b/src/cli/setup_config_file.ts @@ -34,7 +34,7 @@ const REPOSITORY_STRING_KEYS = new Set([ const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush']); const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout']); const AI_BOOLEAN_KEYS = new Set(['pullRequestDescription', 'membersOnly', 'includeReasoning']); -const AI_STRING_KEYS = new Set(['ignoreFiles', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); +const AI_STRING_KEYS = new Set(['ignoreFiles', 'pullRequestDescriptionMode', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); const AI_NUMBER_KEYS = new Set(['bugbotCommentLimit']); const PROJECT_KEYS = new Set([ 'ids', diff --git a/src/cli/setup_prompt_adapter.ts b/src/cli/setup_prompt_adapter.ts index 9f9107ee..1348f91d 100644 --- a/src/cli/setup_prompt_adapter.ts +++ b/src/cli/setup_prompt_adapter.ts @@ -105,6 +105,11 @@ export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromp console.log(color('\n4. Configure AI, projects, and release safety\n', 36)); const ai = defaults.ai; ai.pullRequestDescription = await this.askBoolean('Generate AI pull-request descriptions?', ai.pullRequestDescription); + ai.pullRequestDescriptionMode = await this.askChoice( + 'Pull-request description mode', + ['replace', 'append', 'preserve', 'disabled'], + ai.pullRequestDescriptionMode ?? 'replace', + ) as SetupConfiguration['ai']['pullRequestDescriptionMode']; ai.ignoreFiles = await this.askText('AI ignore file patterns (comma-separated)', ai.ignoreFiles); ai.membersOnly = await this.askBoolean('Restrict AI processing to repository members?', ai.membersOnly); ai.includeReasoning = await this.askBoolean('Include agent reasoning where supported?', ai.includeReasoning); diff --git a/src/data/model/__tests__/pull_request.test.ts b/src/data/model/__tests__/pull_request.test.ts index f4257910..9963227c 100644 --- a/src/data/model/__tests__/pull_request.test.ts +++ b/src/data/model/__tests__/pull_request.test.ts @@ -57,6 +57,26 @@ describe('PullRequest', () => { expect(p.isSynchronize).toBe(true); }); + it('resolves pull request identity from review and check-suite payloads', () => { + const review = new PullRequest(1, 2, 30, { + eventName: 'pull_request_review', + review: { pull_request: { number: 43 } }, + }); + const checkSuite = new PullRequest(1, 2, 30, { + eventName: 'check_suite', + check_suite: { + head_branch: 'feature/43-checks', + pull_requests: [{ number: 43 }], + }, + }); + + expect(review.number).toBe(43); + expect(review.isPullRequest).toBe(true); + expect(checkSuite.number).toBe(43); + expect(checkSuite.head).toBe('feature/43-checks'); + expect(checkSuite.isPullRequest).toBe(true); + }); + it('isPullRequestReviewComment when eventName is pull_request_review_comment', () => { const inputs = { eventName: 'pull_request_review_comment', pull_request: pr }; const p = new PullRequest(1, 2, 30, inputs); diff --git a/src/data/model/ai.ts b/src/data/model/ai.ts index 5491f985..4ebdcc45 100644 --- a/src/data/model/ai.ts +++ b/src/data/model/ai.ts @@ -1,5 +1,10 @@ import { AgentConfiguration, AgentTask, AgentTaskConfiguration } from './agent'; import { defaultAgentCommand } from '../../domain/agent_command'; +import { + DEFAULT_PULL_REQUEST_DESCRIPTION_MODE, + normalizePullRequestDescriptionMode, + type PullRequestDescriptionMode, +} from '../../domain/pull_request_description'; export class Ai { private aiPullRequestDescription: boolean; @@ -10,6 +15,7 @@ export class Ai { private bugbotCommentLimit: number; private bugbotFixVerifyCommands: string[]; private agentTasks: AgentTaskConfiguration; + private pullRequestDescriptionMode: PullRequestDescriptionMode; constructor( _configurationSource: string, @@ -24,7 +30,8 @@ export class Ai { agentTasks: AgentTaskConfiguration = { findings: { provider: 'codex', modelProvider: 'openai', model, command: defaultAgentCommand({ provider: 'codex', modelProvider: 'openai', model }) }, fixer: { provider: 'codex', modelProvider: 'openai', model, command: defaultAgentCommand({ provider: 'codex', modelProvider: 'openai', model }) }, - } + }, + pullRequestDescriptionMode: PullRequestDescriptionMode = DEFAULT_PULL_REQUEST_DESCRIPTION_MODE, ) { this.aiPullRequestDescription = aiPullRequestDescription; this.aiMembersOnly = aiMembersOnly; @@ -34,12 +41,17 @@ export class Ai { this.bugbotCommentLimit = bugbotCommentLimit; this.bugbotFixVerifyCommands = bugbotFixVerifyCommands; this.agentTasks = agentTasks; + this.pullRequestDescriptionMode = normalizePullRequestDescriptionMode(pullRequestDescriptionMode); } getAiPullRequestDescription(): boolean { return this.aiPullRequestDescription; } + getPullRequestDescriptionMode(): PullRequestDescriptionMode { + return this.pullRequestDescriptionMode; + } + getAiMembersOnly(): boolean { return this.aiMembersOnly; } diff --git a/src/data/model/config.ts b/src/data/model/config.ts index 76ad5604..91d36738 100644 --- a/src/data/model/config.ts +++ b/src/data/model/config.ts @@ -3,7 +3,10 @@ import {isRecommendationState, RecommendationState} from "./recommendation_state import {Result} from "./result"; import { asModelInput, readOptionalString, readString } from './model_input'; +export const CONFIG_SCHEMA_VERSION = 1; + export class Config { + readonly schemaVersion: number; branchType: string; releaseBranch: string | undefined; workingBranch: string | undefined; @@ -16,6 +19,11 @@ export class Config { constructor(data: unknown) { const input = asModelInput(data); + this.schemaVersion = typeof input.schemaVersion === 'number' + && Number.isInteger(input.schemaVersion) + && input.schemaVersion > 0 + ? input.schemaVersion + : CONFIG_SCHEMA_VERSION; this.branchType = readString(input, 'branchType'); this.hotfixOriginBranch = readOptionalString(input, 'hotfixOriginBranch'); this.hotfixBranch = readOptionalString(input, 'hotfixBranch'); diff --git a/src/data/model/execution_inputs.ts b/src/data/model/execution_inputs.ts index 5a9c2e85..9edca929 100644 --- a/src/data/model/execution_inputs.ts +++ b/src/data/model/execution_inputs.ts @@ -35,6 +35,29 @@ export interface EventPullRequestPayload { state?: string; } +export interface EventPullRequestReferencePayload { + number?: number; +} + +export interface EventReviewPayload { + state?: string; + pull_request?: EventPullRequestReferencePayload; +} + +export interface EventCheckSuitePayload { + status?: string; + conclusion?: string | null; + head_branch?: string; + pull_requests?: EventPullRequestReferencePayload[]; +} + +export interface EventWorkflowRunPayload { + status?: string; + conclusion?: string | null; + head_branch?: string; + pull_requests?: EventPullRequestReferencePayload[]; +} + export interface EventCommitPayload { id?: string; message?: string; @@ -53,6 +76,9 @@ export interface ExecutionInputs { issue?: EventIssuePayload; label?: EventLabelPayload; pull_request?: EventPullRequestPayload; + review?: EventReviewPayload; + check_suite?: EventCheckSuitePayload; + workflow_run?: EventWorkflowRunPayload; comment?: EventCommentPayload; pull_request_review_comment?: EventCommentPayload; changes?: Record; diff --git a/src/data/model/pull_request.ts b/src/data/model/pull_request.ts index 32f192cd..139a9e6d 100644 --- a/src/data/model/pull_request.ts +++ b/src/data/model/pull_request.ts @@ -24,7 +24,11 @@ export class PullRequest { } get number(): number { - return parsePositiveSafeInteger(this.inputs?.pull_request?.number) ?? -1; + return parsePositiveSafeInteger(this.inputs?.pull_request?.number) + ?? parsePositiveSafeInteger(this.inputs?.review?.pull_request?.number) + ?? uniquePullRequestNumber(this.inputs?.check_suite?.pull_requests) + ?? uniquePullRequestNumber(this.inputs?.workflow_run?.pull_requests) + ?? -1; } get url(): string { @@ -36,7 +40,10 @@ export class PullRequest { } get head(): string { - return this.inputs?.pull_request?.head?.ref ?? ''; + return this.inputs?.pull_request?.head?.ref + ?? this.inputs?.check_suite?.head_branch + ?? this.inputs?.workflow_run?.head_branch + ?? ''; } get base(): string { @@ -66,7 +73,12 @@ export class PullRequest { } get isPullRequest(): boolean { - return this.inputs?.eventName === 'pull_request'; + return [ + 'pull_request', + 'pull_request_review', + 'check_suite', + 'workflow_run', + ].includes(this.inputs?.eventName ?? ''); } get isPullRequestReviewComment(): boolean { @@ -112,3 +124,11 @@ export class PullRequest { this.inputs = inputs; } } + +function uniquePullRequestNumber( + pullRequests: ReadonlyArray<{ number?: number }> | undefined, +): number | undefined { + return pullRequests?.length === 1 + ? parsePositiveSafeInteger(pullRequests[0]?.number) + : undefined; +} diff --git a/src/data/repository/__tests__/pull_request_lifecycle_repository.test.ts b/src/data/repository/__tests__/pull_request_lifecycle_repository.test.ts index b2a85f81..a3bda870 100644 --- a/src/data/repository/__tests__/pull_request_lifecycle_repository.test.ts +++ b/src/data/repository/__tests__/pull_request_lifecycle_repository.test.ts @@ -3,9 +3,10 @@ import { OctokitPullRequestLifecycleClientAdapter } from "../../../infrastructur const mockList = jest.fn(); const mockUpdate = jest.fn(); +const mockGet = jest.fn(); jest.mock("@actions/github", () => ({ - getOctokit: jest.fn(() => ({ rest: { pulls: { list: mockList, update: mockUpdate } } })), + getOctokit: jest.fn(() => ({ rest: { pulls: { list: mockList, update: mockUpdate, get: mockGet } } })), })); jest.mock("../../../utils/logger", () => ({ @@ -17,6 +18,7 @@ describe("PullRequestLifecycleRepository", () => { beforeEach(() => { jest.clearAllMocks(); mockUpdate.mockResolvedValue({}); + mockGet.mockReset(); }); it("lists open pull requests by head branch", async () => { @@ -79,6 +81,18 @@ describe("PullRequestLifecycleRepository", () => { }); }); + it('reads pull-request details for explicit description commands', async () => { + mockGet.mockResolvedValue({ data: { body: 'Human text', head: { ref: 'feature/12' }, base: { ref: 'develop' } } }); + const repository = new PullRequestLifecycleRepository(new OctokitPullRequestLifecycleClientAdapter()); + + await expect(repository.getDetails('owner', 'repo', 12, 'token')).resolves.toEqual({ + body: 'Human text', + headBranch: 'feature/12', + baseBranch: 'develop', + }); + expect(mockGet).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', pull_number: 12 }); + }); + it("returns false for non-success responses and network failures when checking linkage", async () => { const fetchMock = jest.spyOn(global, "fetch") .mockResolvedValueOnce(new Response("not linked", { status: 404 })) diff --git a/src/data/repository/pull_request/pull_request_lifecycle_repository.ts b/src/data/repository/pull_request/pull_request_lifecycle_repository.ts index 78c6fe15..0c76b06c 100644 --- a/src/data/repository/pull_request/pull_request_lifecycle_repository.ts +++ b/src/data/repository/pull_request/pull_request_lifecycle_repository.ts @@ -1,4 +1,5 @@ import { logDebugInfo, logError } from "../../../utils/logger"; +import type { PullRequestDescriptionDetails } from '../../../application/ports/pull_request_description_ports'; import type { GithubClientPort } from "../../../infrastructure/github/ports/github_client_provider_port"; import type { GithubPullRequestLifecycleClient, @@ -147,4 +148,24 @@ export class PullRequestLifecycleRepository { logDebugInfo(`Updated PR #${pullRequestNumber} description with: ${description}`); } + getDetails = async ( + owner: string, + repository: string, + pullRequestNumber: number, + token: string, + ): Promise => { + const octokit = this.githubClient.getClient(token); + if (!octokit.rest.pulls.get) throw new Error('Pull-request details query is not available.'); + const { data } = await octokit.rest.pulls.get({ + owner, + repo: repository, + pull_number: pullRequestNumber, + }); + return { + body: data.body ?? '', + headBranch: data.head?.ref ?? '', + baseBranch: data.base?.ref ?? '', + }; + }; + } diff --git a/src/domain/__tests__/copilot_command.test.ts b/src/domain/__tests__/copilot_command.test.ts index 6caa41e2..e0d78dca 100644 --- a/src/domain/__tests__/copilot_command.test.ts +++ b/src/domain/__tests__/copilot_command.test.ts @@ -4,6 +4,8 @@ describe('Copilot command policy', () => { it.each([ ['/copilot plan', 'plan'], ['/copilot review security regression', 'review'], + ['/copilot status', 'status'], + ['/copilot description', 'description'], ['/copilot fix FINDING-1 FINDING-2', 'fix'], ])('parses %s as an explicit command', (input, name) => { const result = parseCopilotCommand(input); diff --git a/src/domain/__tests__/pull_request_description.test.ts b/src/domain/__tests__/pull_request_description.test.ts new file mode 100644 index 00000000..c5c77817 --- /dev/null +++ b/src/domain/__tests__/pull_request_description.test.ts @@ -0,0 +1,24 @@ +import { + mergeManagedPullRequestDescription, + normalizePullRequestDescriptionMode, + renderManagedPullRequestDescription, +} from '../pull_request_description'; + +describe('pull request description policy', () => { + it('defaults invalid modes to replace', () => { + expect(normalizePullRequestDescriptionMode('unknown')).toBe('replace'); + expect(normalizePullRequestDescriptionMode(' APPEND ')).toBe('append'); + }); + + it('appends a managed section without changing human content', () => { + const result = mergeManagedPullRequestDescription('Human summary', 'Generated details'); + expect(result).toBe(`Human summary\n\n${renderManagedPullRequestDescription('Generated details')}`); + }); + + it('replaces only the existing managed section', () => { + const original = mergeManagedPullRequestDescription('Human summary', 'Old details'); + expect(mergeManagedPullRequestDescription(original, 'New details')).toBe( + `Human summary\n\n${renderManagedPullRequestDescription('New details')}`, + ); + }); +}); diff --git a/src/domain/copilot_command.ts b/src/domain/copilot_command.ts index 72c76006..d328445e 100644 --- a/src/domain/copilot_command.ts +++ b/src/domain/copilot_command.ts @@ -5,6 +5,7 @@ export const COPILOT_COMMAND_NAMES = [ 'estimate', 'test-plan', 'status', + 'description', 'review', 'findings', 'fix', diff --git a/src/domain/pull_request_description.ts b/src/domain/pull_request_description.ts new file mode 100644 index 00000000..165865cb --- /dev/null +++ b/src/domain/pull_request_description.ts @@ -0,0 +1,54 @@ +export const PULL_REQUEST_DESCRIPTION_MODES = [ + 'replace', + 'append', + 'preserve', + 'disabled', +] as const; + +export type PullRequestDescriptionMode = typeof PULL_REQUEST_DESCRIPTION_MODES[number]; + +export const DEFAULT_PULL_REQUEST_DESCRIPTION_MODE: PullRequestDescriptionMode = 'replace'; + +export const MANAGED_PULL_REQUEST_DESCRIPTION_START = ''; +export const MANAGED_PULL_REQUEST_DESCRIPTION_END = ''; + +/** Normalizes public configuration while keeping invalid values safe and backwards compatible. */ +export function normalizePullRequestDescriptionMode(value: unknown): PullRequestDescriptionMode { + const normalized = String(value ?? '').trim().toLowerCase(); + return PULL_REQUEST_DESCRIPTION_MODES.includes(normalized as PullRequestDescriptionMode) + ? normalized as PullRequestDescriptionMode + : DEFAULT_PULL_REQUEST_DESCRIPTION_MODE; +} + +export function hasManagedPullRequestDescription(body: unknown): boolean { + return typeof body === 'string' && body.includes(MANAGED_PULL_REQUEST_DESCRIPTION_START); +} + +/** Renders one bounded Copilot-owned section without taking ownership of the rest of the body. */ +export function renderManagedPullRequestDescription(generated: string): string { + return [ + MANAGED_PULL_REQUEST_DESCRIPTION_START, + generated.trim(), + MANAGED_PULL_REQUEST_DESCRIPTION_END, + ].join('\n'); +} + +/** Replaces the existing managed section, or appends one when none exists. */ +export function mergeManagedPullRequestDescription(currentBody: unknown, generated: string): string { + const current = typeof currentBody === 'string' ? currentBody.trim() : ''; + const managed = renderManagedPullRequestDescription(generated); + const start = current.indexOf(MANAGED_PULL_REQUEST_DESCRIPTION_START); + const end = current.indexOf(MANAGED_PULL_REQUEST_DESCRIPTION_END, start + MANAGED_PULL_REQUEST_DESCRIPTION_START.length); + + if (start >= 0 && end >= start) { + const before = current.slice(0, start).trimEnd(); + const after = current.slice(end + MANAGED_PULL_REQUEST_DESCRIPTION_END.length).trimStart(); + return [before, managed, after].filter(Boolean).join('\n\n').trim(); + } + + return current ? `${current}\n\n${managed}` : managed; +} + +export function shouldAutomaticallyUpdatePullRequestDescription(mode: PullRequestDescriptionMode): boolean { + return mode === 'replace' || mode === 'append'; +} diff --git a/src/domain/setup.ts b/src/domain/setup.ts index 1524cf34..3b3eaf1b 100644 --- a/src/domain/setup.ts +++ b/src/domain/setup.ts @@ -1,4 +1,5 @@ import type { AgentProvider, AgentTask } from './agent'; +import type { PullRequestDescriptionMode } from './pull_request_description'; export type SetupFeature = | 'issues' @@ -47,6 +48,8 @@ export interface SetupRepositoryConfiguration { export interface SetupAiConfiguration { pullRequestDescription: boolean; + /** Optional for backwards-compatible setup files created before v3.3.0. */ + pullRequestDescriptionMode?: PullRequestDescriptionMode; ignoreFiles: string; membersOnly: boolean; includeReasoning: boolean; diff --git a/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts b/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts index 997c94db..4dba8112 100644 --- a/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts @@ -118,6 +118,7 @@ describe("main run route composition root", () => { gitCommit, expect.anything(), expect.anything(), + expect.anything(), ); }); @@ -138,6 +139,7 @@ describe("main run route composition root", () => { expect.anything(), expect.anything(), expect.anything(), + expect.anything(), ); }); diff --git a/src/infrastructure/composition/main_run_route_composition_root.ts b/src/infrastructure/composition/main_run_route_composition_root.ts index f1b6f28e..74ceec6f 100644 --- a/src/infrastructure/composition/main_run_route_composition_root.ts +++ b/src/infrastructure/composition/main_run_route_composition_root.ts @@ -22,7 +22,6 @@ import { CheckPullRequestCommentLanguageUseCase } from "../../application/usecas import { CommentLanguageTranslationWorkflow } from "../../application/usecases/steps/common/comment_language_translation_workflow"; import { BranchCompareRepository } from "../../data/repository/branch_compare_repository"; import { MergeRepository } from "../../data/repository/merge_repository"; -import { PullRequestLifecycleRepository } from "../../data/repository/pull_request/pull_request_lifecycle_repository"; import { RepositoryReleasePublicationRepository } from "../../data/repository/release/repository_release_publication_repository"; import { RepositoryTagRepository } from "../../data/repository/release/repository_tag_repository"; import { GitCommitAdapter } from "../git_commit_adapter"; @@ -50,6 +49,9 @@ import { import { createIssueLabelRepository } from "./issue_labels_composition_root"; import { createIssueUseCaseCompositionRoot } from "./issue_use_case_composition_root"; import { createPullRequestUseCaseCompositionRoot } from "./pull_request_use_case_composition_root"; +import { createOrganizationMembersCompositionRoot } from "./organization_members_composition_root"; +import { UpdatePullRequestDescriptionUseCase } from "../../application/usecases/steps/pull_request/update_pull_request_description_use_case"; +import { PullRequestLifecycleRepository } from "../../data/repository/pull_request/pull_request_lifecycle_repository"; function createDetectPotentialProblemsUseCase(): DetectPotentialProblemsUseCase { const bugbot = createBugbotCompositionRoot(); @@ -97,6 +99,12 @@ export function createIssueCommentUseCaseCompositionRoot(): IssueCommentUseCase const language = createLanguageQueryPort(); const fixer = createFixerQueryPort(); const gitCommit = new GitCommitAdapter(); + const pullRequestDescription = new UpdatePullRequestDescriptionUseCase( + new PullRequestLifecycleRepository(createPullRequestLifecycleClient()), + createIssueContentCompositionRoot(), + createOrganizationMembersCompositionRoot(), + createFindingsQueryPort(), + ); return new IssueCommentUseCase( new CheckIssueCommentLanguageUseCase( @@ -121,6 +129,7 @@ export function createIssueCommentUseCaseCompositionRoot(): IssueCommentUseCase gitCommit, new DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution), + pullRequestDescription, ); } @@ -130,6 +139,12 @@ export function createPullRequestReviewCommentUseCaseCompositionRoot(): PullRequ const language = createLanguageQueryPort(); const fixer = createFixerQueryPort(); const gitCommit = new GitCommitAdapter(); + const pullRequestDescription = new UpdatePullRequestDescriptionUseCase( + new PullRequestLifecycleRepository(createPullRequestLifecycleClient()), + createIssueContentCompositionRoot(), + createOrganizationMembersCompositionRoot(), + createFindingsQueryPort(), + ); return new PullRequestReviewCommentUseCase( new CheckPullRequestCommentLanguageUseCase( @@ -154,6 +169,7 @@ export function createPullRequestReviewCommentUseCaseCompositionRoot(): PullRequ gitCommit, new DismissBugbotFindingsUseCase({ contextPorts: bugbot.context, resolutionPorts: bugbot.resolution }), new DetectPotentialProblemsUseCase(findings, bugbot.context, bugbot.publication, bugbot.resolution), + pullRequestDescription, ); } @@ -176,20 +192,32 @@ export function createCommitUseCaseCompositionRoot( export function createMainRunRouteCompositionRoot( projectBoardCommandPort: ProjectBoardCommandPort, ): MainRunRouteHandlers { + // Composition is scoped to one main run. Each route is built only when it is + // actually selected, while repeated calls in the same run reuse its graph. + const singleAction = lazy(() => createSingleActionUseCaseCompositionRoot()); + const issueComment = lazy(() => createIssueCommentUseCaseCompositionRoot()); + const issue = lazy(() => createIssueUseCaseCompositionRoot()); + const pullRequestReviewComment = lazy(() => createPullRequestReviewCommentUseCaseCompositionRoot()); + const pullRequest = lazy(() => createPullRequestUseCaseCompositionRoot()); + const push = lazy(() => createCommitUseCaseCompositionRoot(projectBoardCommandPort)); + return { "single-action": async (execution) => - createSingleActionUseCaseCompositionRoot().invoke(execution), + singleAction().invoke(execution), "issue-comment": async (execution) => - createIssueCommentUseCaseCompositionRoot().invoke(execution), + issueComment().invoke(execution), issue: async (execution) => - createIssueUseCaseCompositionRoot().invoke(execution), + issue().invoke(execution), "pull-request-review-comment": async (execution) => - createPullRequestReviewCommentUseCaseCompositionRoot().invoke(execution), + pullRequestReviewComment().invoke(execution), "pull-request": async (execution) => - createPullRequestUseCaseCompositionRoot().invoke(execution), + pullRequest().invoke(execution), push: async (execution) => - createCommitUseCaseCompositionRoot(projectBoardCommandPort).invoke( - execution, - ), + push().invoke(execution), }; } + +function lazy(factory: () => T): () => T { + let value: T | undefined; + return () => value ?? (value = factory()); +} diff --git a/src/infrastructure/github/ports/github_pull_request_provider_ports.ts b/src/infrastructure/github/ports/github_pull_request_provider_ports.ts index cb26b2d3..b305d71e 100644 --- a/src/infrastructure/github/ports/github_pull_request_provider_ports.ts +++ b/src/infrastructure/github/ports/github_pull_request_provider_ports.ts @@ -34,6 +34,11 @@ export interface GithubPullRequestLifecycleClient { parameters: Record, ): Promise<{ data: GithubPullRequestSummary[] }>; update(parameters: Record): Promise; + get?(parameters: Record): Promise<{ data: { + body?: string | null; + head?: { ref?: string | null }; + base?: { ref?: string | null }; + } }>; }; }; } diff --git a/src/manager/description/configuration_payload_policy.ts b/src/manager/description/configuration_payload_policy.ts index dc8d4075..34264ed6 100644 --- a/src/manager/description/configuration_payload_policy.ts +++ b/src/manager/description/configuration_payload_policy.ts @@ -1,8 +1,10 @@ import type { Execution } from '../../data/model/execution'; +import { CONFIG_SCHEMA_VERSION } from '../../data/model/config'; export function buildConfigurationPayload(execution: Execution, storedRaw: string | undefined): string { const current = execution.currentConfiguration; const payload: Record = { + schemaVersion: CONFIG_SCHEMA_VERSION, branchType: current.branchType, releaseBranch: current.releaseBranch, workingBranch: current.workingBranch, diff --git a/src/tooling/__tests__/collect_architecture_metrics.test.ts b/src/tooling/__tests__/collect_architecture_metrics.test.ts index 305bd6ba..14948bd1 100644 --- a/src/tooling/__tests__/collect_architecture_metrics.test.ts +++ b/src/tooling/__tests__/collect_architecture_metrics.test.ts @@ -64,7 +64,7 @@ describe("collect architecture metrics", () => { runProcess( [execPath, "-e", "setTimeout(() => {}, 700)"], process.cwd(), - 1000, + 3000, ); expect(existsSync(marker)).toBe(false); }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1f410380..1430d370 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -260,6 +260,7 @@ export const INPUT_KEYS = { // AI configuration AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', + AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', AI_MEMBERS_ONLY: 'ai-members-only', AI_IGNORE_FILES: 'ai-ignore-files', AI_INCLUDE_REASONING: 'ai-include-reasoning', From c2c2e83784d372e74331d1aeed57b6e0c97ceb93 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Thu, 3 Sep 2026 05:06:27 +0200 Subject: [PATCH 05/11] develop: refactor application boundaries and harden persistence --- build/cli/index.js | 143 +++++++++++----- .../policies/agent_activity_policy.d.ts | 32 +++- .../configuration_persistence_policy.d.ts | 9 +- .../policies/deploy_workflow_policy.d.ts | 29 +++- .../policies/status_command_policy.d.ts | 42 ++++- .../synchronize_lifecycle_state_use_case.d.ts | 33 +++- build/cli/src/data/model/config.d.ts | 15 +- .../configuration_payload_policy.d.ts | 15 +- build/github_action/index.js | 133 ++++++++++----- .../policies/agent_activity_policy.d.ts | 32 +++- .../configuration_persistence_policy.d.ts | 9 +- .../policies/deploy_workflow_policy.d.ts | 29 +++- .../policies/status_command_policy.d.ts | 42 ++++- .../synchronize_lifecycle_state_use_case.d.ts | 33 +++- .../github_action/src/data/model/config.d.ts | 15 +- .../configuration_payload_policy.d.ts | 15 +- docs/development/architecture.mdx | 13 ++ docs/development/testing.mdx | 11 ++ scripts/collect-architecture-metrics.cjs | 3 +- .../__tests__/architecture_boundaries.test.ts | 24 +++ .../__tests__/application_error.test.ts | 31 ++++ .../policies/agent_activity_policy.ts | 41 ++++- .../policies/agent_command_parser.ts | 8 +- .../agent_command_validation_policy.ts | 23 +-- .../agent_configuration_validation_policy.ts | 13 +- .../configuration_persistence_policy.ts | 9 +- .../policies/deploy_workflow_policy.ts | 17 +- .../policies/status_command_policy.ts | 35 +++- ...lifecycle_event_replay.integration.test.ts | 160 ++++++++++++++++++ .../usecases/actions/create_release_policy.ts | 3 +- .../synchronize_lifecycle_state_use_case.ts | 38 ++++- ...lve_github_execution_admission_use_case.ts | 3 +- .../execution/setup_execution_workflow.ts | 3 +- .../setup/setup_credentials_use_case.ts | 11 +- .../usecases/setup/setup_wizard_use_case.ts | 6 +- .../bugbot/bugbot_autofix_postflight.ts | 7 +- .../commit/bugbot/bugbot_autofix_preflight.ts | 7 +- .../usecases/steps/commit/bugbot/marker.ts | 4 +- .../common/store_configuration_use_case.ts | 3 +- ...ait_for_previous_workflow_runs_use_case.ts | 11 +- src/data/model/__tests__/config.test.ts | 44 ++++- src/data/model/config.ts | 56 +++++- .../__tests__/configuration_handler.test.ts | 24 +++ .../configuration_payload_policy.ts | 33 +++- .../collect_architecture_metrics.test.ts | 3 + 45 files changed, 1089 insertions(+), 181 deletions(-) create mode 100644 src/application/errors/__tests__/application_error.test.ts create mode 100644 src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts diff --git a/build/cli/index.js b/build/cli/index.js index a4f16dca..b582751a 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -55714,15 +55714,16 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.parseAgentCommand = parseAgentCommand; const shellQuote = __importStar(__nccwpck_require__(75430)); +const application_error_1 = __nccwpck_require__(75999); /** Parses a literal agent command without allowing shell operators or substitutions. */ function parseAgentCommand(command) { const trimmed = command.trim(); if (!trimmed) - throw new Error('Agent CLI command must not be empty.'); + throw new application_error_1.ApplicationError('Agent CLI command must not be empty.', 'validation'); const parsed = shellQuote.parse(trimmed, {}); const argv = parsed.filter((entry) => typeof entry === 'string'); if (argv.length !== parsed.length || argv.length === 0) { - throw new Error('Agent CLI command contains unsupported shell syntax. Use an executable and literal arguments only.'); + throw new application_error_1.ApplicationError('Agent CLI command contains unsupported shell syntax. Use an executable and literal arguments only.', 'validation'); } return { executable: argv[0], args: argv.slice(1) }; } @@ -55767,11 +55768,12 @@ function cliInstallationHint(provider) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.validateConfiguredAgentCommand = validateConfiguredAgentCommand; +const application_error_1 = __nccwpck_require__(75999); const agent_command_parser_1 = __nccwpck_require__(15044); function validateConfiguredAgentCommand(configuration) { const command = configuration.command?.trim(); if (!command) - throw new Error(`CLI command is required for ${configuration.provider}.`); + throw new application_error_1.ApplicationError(`CLI command is required for ${configuration.provider}.`, 'validation'); const { args } = (0, agent_command_parser_1.parseAgentCommand)(command); validateCommandShape(configuration, args); validateModelSelection(configuration, args); @@ -55780,13 +55782,13 @@ function validateConfiguredAgentCommand(configuration) { } function validateCommandShape(configuration, args) { if (configuration.provider !== 'codex' && args.includes('-')) { - throw new Error(`${configuration.provider} command must not include the Codex stdin placeholder "-"; its prompt is passed as an argument.`); + throw new application_error_1.ApplicationError(`${configuration.provider} command must not include the Codex stdin placeholder "-"; its prompt is passed as an argument.`, 'validation'); } if (configuration.provider === 'codex' && args.at(-1) !== '-') { - throw new Error('Codex command must end with the stdin placeholder "-".'); + throw new application_error_1.ApplicationError('Codex command must end with the stdin placeholder "-".', 'validation'); } if (!hasFlag(args, '--model') && !hasFlag(args, '-m')) { - throw new Error(`${configuration.provider} command must select the model explicitly with --model.`); + throw new application_error_1.ApplicationError(`${configuration.provider} command must select the model explicitly with --model.`, 'validation'); } } function validateModelSelection(configuration, args) { @@ -55795,18 +55797,18 @@ function validateModelSelection(configuration, args) { : configuration.model.trim(); const configuredModel = flagValue(args, ['--model', '-m']); if (configuredModel !== expectedModel) { - throw new Error(`${configuration.provider} command must select configured model "${expectedModel}".`); + throw new application_error_1.ApplicationError(`${configuration.provider} command must select configured model "${expectedModel}".`, 'validation'); } } function validateProviderConfiguration(configuration, args) { if (configuration.provider !== 'codex') return; if (!hasConfig(args, 'model_provider')) { - throw new Error('Codex command must select the model provider explicitly with --config model_provider=... .'); + throw new application_error_1.ApplicationError('Codex command must select the model provider explicitly with --config model_provider=... .', 'validation'); } const expectedProvider = configuration.modelProvider?.trim() || 'openai'; if (configValue(args, 'model_provider') !== expectedProvider) { - throw new Error(`Codex command must select configured model provider "${expectedProvider}".`); + throw new application_error_1.ApplicationError(`Codex command must select configured model provider "${expectedProvider}".`, 'validation'); } } function validateEffortSelection(configuration, args) { @@ -55815,10 +55817,10 @@ function validateEffortSelection(configuration, args) { return; if (configuration.provider === 'codex') { if (!hasConfig(args, 'model_reasoning_effort')) { - throw new Error('Codex command must select effort explicitly with --config model_reasoning_effort=... .'); + throw new application_error_1.ApplicationError('Codex command must select effort explicitly with --config model_reasoning_effort=... .', 'validation'); } if (configValue(args, 'model_reasoning_effort') !== effort) { - throw new Error(`Codex command must select configured effort "${effort}".`); + throw new application_error_1.ApplicationError(`Codex command must select configured effort "${effort}".`, 'validation'); } return; } @@ -55830,10 +55832,10 @@ function validateEffortSelection(configuration, args) { return; } if (!hasFlag(args, '--variant')) { - throw new Error('OpenCode command must select effort explicitly with --variant ... .'); + throw new application_error_1.ApplicationError('OpenCode command must select effort explicitly with --variant ... .', 'validation'); } if (flagValue(args, ['--variant']) !== effort) { - throw new Error(`OpenCode command must select configured effort "${effort}".`); + throw new application_error_1.ApplicationError(`OpenCode command must select configured effort "${effort}".`, 'validation'); } } function hasFlag(args, flag) { @@ -55923,7 +55925,7 @@ function hasTaskOverride(value) { /***/ }), /***/ 60596: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -55934,11 +55936,12 @@ exports.resolveModelProvider = resolveModelProvider; exports.resolveModel = resolveModel; exports.resolveEffort = resolveEffort; exports.assertModelAllowlisted = assertModelAllowlisted; +const application_error_1 = __nccwpck_require__(75999); exports.SUPPORTED_AGENT_PROVIDERS = ['opencode', 'cursor', 'codex']; function resolveAgentProvider(value) { if (exports.SUPPORTED_AGENT_PROVIDERS.includes(value)) return value; - throw new Error(`Unsupported agent provider "${value}". Supported providers: ${exports.SUPPORTED_AGENT_PROVIDERS.join(', ')}.`); + throw new application_error_1.ApplicationError(`Unsupported agent provider "${value}". Supported providers: ${exports.SUPPORTED_AGENT_PROVIDERS.join(', ')}.`, 'validation'); } function resolveModelProvider(value, environment) { const provider = value?.trim().toLowerCase() || 'openai'; @@ -55949,7 +55952,7 @@ function resolveModelProvider(value, environment) { function resolveModel(value) { const model = value.trim(); if (!model) - throw new Error('Agent model must not be empty.'); + throw new application_error_1.ApplicationError('Agent model must not be empty.', 'validation'); assertIdentifier(model, 'Agent model must be a simple model identifier without whitespace or shell syntax.', /^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/); return model; } @@ -55962,25 +55965,25 @@ function resolveEffort(value) { function assertModelAllowlisted(modelProvider, model, environment) { const allowedModels = parseAllowlist(environment.AGENT_ALLOWED_MODELS); if (allowedModels.length > 0 && !allowedModels.includes(`${modelProvider}/${model}`) && !allowedModels.includes(model)) { - throw new Error(`Agent model "${modelProvider}/${model}" is not allowlisted.`); + throw new application_error_1.ApplicationError(`Agent model "${modelProvider}/${model}" is not allowlisted.`, 'authorization'); } } function assertAllowlisted(name, value, environment) { const values = parseAllowlist(environment[name]); if (values.length > 0 && !values.includes(value)) - throw new Error(`Agent model provider "${value}" is not allowlisted.`); + throw new application_error_1.ApplicationError(`Agent model provider "${value}" is not allowlisted.`, 'authorization'); } function parseAllowlist(raw) { if (!raw?.trim()) return []; const values = raw.split(',').map(value => value.trim().toLowerCase()).filter(Boolean); if (values.length === 0) - throw new Error('Agent allowlist must contain at least one value.'); + throw new application_error_1.ApplicationError('Agent allowlist must contain at least one value.', 'configuration'); return values; } function assertIdentifier(value, message, pattern = /^[a-z0-9][a-z0-9_-]*$/i) { if (!pattern.test(value)) - throw new Error(message); + throw new application_error_1.ApplicationError(message, 'validation'); } @@ -57454,6 +57457,7 @@ exports.validateReleaseInput = validateReleaseInput; exports.normalizeVersion = normalizeVersion; exports.versionForRelease = versionForRelease; const constants_1 = __nccwpck_require__(15415); +const application_error_1 = __nccwpck_require__(75999); const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/; function validateReleaseInput(input) { if (!input.version.length) @@ -57474,7 +57478,7 @@ function normalizeVersion(version) { function versionForRelease(version) { const normalized = normalizeVersion(version); if (normalized === undefined) - throw new Error('Cannot build a release version from invalid input.'); + throw new application_error_1.ApplicationError('Cannot build a release version from invalid input.', 'validation'); return `v${normalized}`; } @@ -59196,6 +59200,7 @@ exports.SetupExecutionUseCase = SetupExecutionUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runSetupExecution = runSetupExecution; +const application_error_1 = __nccwpck_require__(75999); const initial_labels_policy_1 = __nccwpck_require__(50293); const previous_branch_state_policy_1 = __nccwpck_require__(43630); const logging_ports_1 = __nccwpck_require__(6152); @@ -59223,7 +59228,7 @@ async function loadTokenUser(execution, organizationSetupPort) { return; execution.tokenUser = await organizationSetupPort.getUserFromToken(execution.tokens.token); if (!execution.tokenUser) - throw new Error('Failed to get user from token'); + throw new application_error_1.ApplicationError('Failed to get user from token', 'authorization'); } async function loadPreviousConfiguration(execution, configurationPort) { const issueNumber = configurationIssueNumber(execution); @@ -59681,12 +59686,13 @@ Object.defineProperty(exports, "SetupCredentialsUseCase", ({ enumerable: true, g /***/ }), /***/ 67438: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupCredentialsUseCase = void 0; +const application_error_1 = __nccwpck_require__(75999); /** Coordinates secret collection and validation without placing secret values in config files. */ class SetupCredentialsUseCase { constructor(prompt, validation, secrets, remoteHealth) { @@ -59698,14 +59704,14 @@ class SetupCredentialsUseCase { async collect(request) { const setupCheck = await this.validation.validateSetupPat(request.owner, request.repository, request.setupToken); if (setupCheck.status !== 'valid') { - throw new Error(`Setup PAT validation failed: ${setupCheck.message}`); + throw new application_error_1.ApplicationError(`Setup PAT validation failed: ${setupCheck.message}`, 'authorization'); } if (!request.manageSecrets) { this.prompt.showCredentialChecks([setupCheck]); return { collection: { apiKeys: [] }, checks: [setupCheck], existingSecretNames: [] }; } if (!this.secrets) - throw new Error('Repository Secret provisioning is not available in this installation.'); + throw new application_error_1.ApplicationError('Repository Secret provisioning is not available in this installation.', 'configuration'); const existingSecretNames = await this.secrets.list(request.owner, request.repository, request.setupToken); const requirements = request.requirements.filter(requirement => requirement.name !== 'SETUP_PAT'); this.prompt.explainCredentialSeparation(requirements); @@ -59727,7 +59733,7 @@ class SetupCredentialsUseCase { checks.push(remoteCheck); const decision = await this.prompt.chooseExistingCredential(requirement, remoteCheck); if (remoteCheck.status === 'invalid' && decision !== 'replace') { - throw new Error(`${requirement.name} is invalid and must be replaced before setup can continue.`); + throw new application_error_1.ApplicationError(`${requirement.name} is invalid and must be replaced before setup can continue.`, 'authorization'); } if (decision === 'keep') continue; @@ -59740,14 +59746,14 @@ class SetupCredentialsUseCase { if (!value) { if (!existing) checks.push({ name: requirement.name, status: 'missing', message: 'No value was provided.' }); - throw new Error(`${requirement.name} is required by the selected workflows.`); + throw new application_error_1.ApplicationError(`${requirement.name} is required by the selected workflows.`, 'configuration'); } const check = requirement.kind === 'workflowPat' ? await this.validation.validateSetupPat(request.owner, request.repository, value.value) : await this.validation.validateCredential(requirement, value.value); checks.push({ ...check, name: requirement.name }); if (check.status !== 'valid') { - throw new Error(`${requirement.name} validation failed: ${check.message}`); + throw new application_error_1.ApplicationError(`${requirement.name} validation failed: ${check.message}`, 'authorization'); } values.push(value); } @@ -59774,6 +59780,7 @@ exports.SetupCredentialsUseCase = SetupCredentialsUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupWizardUseCase = void 0; +const application_error_1 = __nccwpck_require__(75999); const setup_configuration_policy_1 = __nccwpck_require__(56637); class SetupWizardUseCase { constructor(prompt) { @@ -59790,7 +59797,7 @@ class SetupWizardUseCase { : collected; const validationErrors = (0, setup_configuration_policy_1.validateSetupConfiguration)(configuration); if (validationErrors.length > 0) { - throw new Error(`Invalid setup configuration:\n${validationErrors.map(error => `- ${error}`).join('\n')}`); + throw new application_error_1.ApplicationError(`Invalid setup configuration:\n${validationErrors.map(error => `- ${error}`).join('\n')}`, 'validation'); } const plan = (0, setup_configuration_policy_1.buildSetupPlan)(configuration); this.prompt.showPlan(plan); @@ -59993,6 +60000,7 @@ async function runUserRequestCommitAndPush(execution, options, authenticatedUser Object.defineProperty(exports, "__esModule", ({ value: true })); exports.finalizeBugbotAutofix = finalizeBugbotAutofix; const result_1 = __nccwpck_require__(73817); +const application_error_1 = __nccwpck_require__(75999); const workspace_changes_1 = __nccwpck_require__(93370); const logging_ports_1 = __nccwpck_require__(6152); async function finalizeBugbotAutofix(execution, context, idsToFix, workspacePathsBefore, responseText, gitCommitPort) { @@ -60025,7 +60033,7 @@ async function inspectWorkspace(gitCommitPort, phase) { return await (0, workspace_changes_1.listWorkspacePaths)(gitCommitPort); } catch (error) { - throw new Error(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`); + throw new application_error_1.ApplicationError(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`, 'provider', { cause: error, retryable: true }); } } function failure(message) { @@ -60043,6 +60051,7 @@ function failure(message) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prepareBugbotAutofix = prepareBugbotAutofix; const result_1 = __nccwpck_require__(73817); +const application_error_1 = __nccwpck_require__(75999); const types_1 = __nccwpck_require__(32632); const build_bugbot_fix_prompt_1 = __nccwpck_require__(89819); const load_bugbot_context_use_case_1 = __nccwpck_require__(4050); @@ -60076,7 +60085,7 @@ async function inspectWorkspace(gitCommitPort, phase) { return await (0, workspace_changes_1.listWorkspacePaths)(gitCommitPort); } catch (error) { - throw new Error(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`); + throw new application_error_1.ApplicationError(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`, 'provider', { cause: error, retryable: true }); } } function failure(message) { @@ -61448,6 +61457,7 @@ exports.replaceMarkerInBody = replaceMarkerInBody; exports.extractTitleFromBody = extractTitleFromBody; exports.buildCommentBody = buildCommentBody; const constants_1 = __nccwpck_require__(15415); +const application_error_1 = __nccwpck_require__(75999); const github_comment_publication_policy_1 = __nccwpck_require__(72712); /** Maximum lossless finding identity accepted by the marker contract. */ exports.MAX_FINDING_ID_LENGTH = 200; @@ -61471,11 +61481,11 @@ function normalizeFindingIdForMarker(findingId) { function requireFindingIdForMarker(findingId) { const safeId = normalizeFindingIdForMarker(findingId); if (safeId == null) { - throw new Error(findingId.trim().length === 0 + throw new application_error_1.ApplicationError(findingId.trim().length === 0 ? "Finding ID is empty after marker sanitization." : findingId.trim().length > exports.MAX_FINDING_ID_LENGTH ? "Finding ID exceeds the maximum marker length." - : "Finding ID contains marker-breaking characters."); + : "Finding ID contains marker-breaking characters.", 'validation'); } return safeId; } @@ -65780,6 +65790,7 @@ exports.UpgradeCliUseCase = UpgradeCliUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.WaitForPreviousWorkflowRunsUseCase = void 0; const workflow_queue_policy_1 = __nccwpck_require__(43193); +const application_error_1 = __nccwpck_require__(75999); const SYSTEM_CLOCK = { nowMilliseconds: () => Date.now() }; const SYSTEM_RANDOM = { next: () => Math.random() }; class WaitForPreviousWorkflowRunsUseCase { @@ -65797,13 +65808,13 @@ class WaitForPreviousWorkflowRunsUseCase { let pollIndex = 0; while (true) { if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } const activeRunCount = await this.queryPort.countActivePreviousRuns(query, { deadlineAtMilliseconds, }); if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } if (activeRunCount === 0) { this.observerPort.noActivePreviousRuns(); @@ -65811,7 +65822,7 @@ class WaitForPreviousWorkflowRunsUseCase { } const delayMilliseconds = (0, workflow_queue_policy_1.calculateWorkflowPollingDelay)(pollIndex, this.random.next(), this.policy); if (this.clock.nowMilliseconds() + delayMilliseconds >= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } this.observerPort.waitingForPreviousRuns(activeRunCount, delayMilliseconds); await this.delayPort.wait(delayMilliseconds); @@ -65820,6 +65831,9 @@ class WaitForPreviousWorkflowRunsUseCase { } } exports.WaitForPreviousWorkflowRunsUseCase = WaitForPreviousWorkflowRunsUseCase; +function queueTimeoutError() { + return new application_error_1.ApplicationError('Timeout waiting for previous runs to finish.', 'workflow', { retryable: true }); +} /***/ }), @@ -67703,19 +67717,51 @@ exports.Commit = Commit; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Config = exports.CONFIG_SCHEMA_VERSION = void 0; +exports.migrateConfigurationPayload = migrateConfigurationPayload; const branch_configuration_1 = __nccwpck_require__(71934); const recommendation_state_1 = __nccwpck_require__(68514); const model_input_1 = __nccwpck_require__(14637); -exports.CONFIG_SCHEMA_VERSION = 1; +/** Version of the durable configuration contract stored in issue/PR content. */ +exports.CONFIG_SCHEMA_VERSION = 2; +/** + * Normalizes persisted configuration without silently losing fields from a + * newer installation. Unknown keys are deliberately retained so a downgrade + * or a mixed-version workflow can round-trip data safely. + */ +function migrateConfigurationPayload(value) { + const original = { ...(0, model_input_1.asModelInput)(value) }; + const sourceVersion = readSchemaVersion(original['schemaVersion']); + if (sourceVersion > exports.CONFIG_SCHEMA_VERSION) { + return { + payload: original, + sourceVersion, + migrated: false, + futureVersion: true, + }; + } + const payload = { ...original }; + const hadTransientResults = Object.prototype.hasOwnProperty.call(payload, 'results'); + delete payload.results; + if (payload.branchConfiguration === null) + delete payload.branchConfiguration; + if (!(0, recommendation_state_1.isRecommendationState)(payload.recommendationState)) + delete payload.recommendationState; + payload.schemaVersion = exports.CONFIG_SCHEMA_VERSION; + return { + payload, + sourceVersion, + migrated: sourceVersion !== exports.CONFIG_SCHEMA_VERSION || hadTransientResults, + futureVersion: false, + }; +} +function readSchemaVersion(value) { + return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : 0; +} class Config { constructor(data) { this.results = []; - const input = (0, model_input_1.asModelInput)(data); - this.schemaVersion = typeof input.schemaVersion === 'number' - && Number.isInteger(input.schemaVersion) - && input.schemaVersion > 0 - ? input.schemaVersion - : exports.CONFIG_SCHEMA_VERSION; + const input = (0, model_input_1.asModelInput)(migrateConfigurationPayload(data).payload); + this.schemaVersion = readSchemaVersion(input.schemaVersion) || exports.CONFIG_SCHEMA_VERSION; this.branchType = (0, model_input_1.readString)(input, 'branchType'); this.hotfixOriginBranch = (0, model_input_1.readOptionalString)(input, 'hotfixOriginBranch'); this.hotfixBranch = (0, model_input_1.readOptionalString)(input, 'hotfixBranch'); @@ -76722,6 +76768,7 @@ exports.buildConfigurationPayload = buildConfigurationPayload; const config_1 = __nccwpck_require__(90450); function buildConfigurationPayload(execution, storedRaw) { const current = execution.currentConfiguration; + const stored = parseStoredConfiguration(storedRaw); const payload = { schemaVersion: config_1.CONFIG_SCHEMA_VERSION, branchType: current.branchType, @@ -76733,7 +76780,8 @@ function buildConfigurationPayload(execution, storedRaw) { branchConfiguration: current.branchConfiguration, recommendationState: current.recommendationState, }; - mergeMissingValues(payload, parseStoredConfiguration(storedRaw)); + mergeMissingValues(payload, stored); + preserveFutureSchemaVersion(payload, stored); delete payload.results; return JSON.stringify(payload, null, 4); } @@ -76741,7 +76789,7 @@ function parseStoredConfiguration(storedRaw) { if (!storedRaw?.trim()) return undefined; try { - return JSON.parse(storedRaw); + return (0, config_1.migrateConfigurationPayload)(JSON.parse(storedRaw)).payload; } catch { return undefined; @@ -76755,6 +76803,11 @@ function mergeMissingValues(payload, stored) { payload[key] = stored[key]; } } +function preserveFutureSchemaVersion(payload, stored) { + if (typeof stored?.schemaVersion === 'number' && stored.schemaVersion > config_1.CONFIG_SCHEMA_VERSION) { + payload.schemaVersion = stored.schemaVersion; + } +} /***/ }), diff --git a/build/cli/src/application/policies/agent_activity_policy.d.ts b/build/cli/src/application/policies/agent_activity_policy.d.ts index b26fe59f..01511480 100644 --- a/build/cli/src/application/policies/agent_activity_policy.d.ts +++ b/build/cli/src/application/policies/agent_activity_policy.d.ts @@ -1,4 +1,32 @@ -import type { Execution } from '../../data/model/execution'; +import type { AgentConfiguration, AgentTask } from '../../domain/agent'; export type AgentActivityRoute = 'single-action' | 'issue-comment' | 'issue' | 'pull-request-review-comment' | 'pull-request' | 'push'; +export interface AgentActivityExecutionContext { + readonly eventName: string; + readonly issueNumber: number; + readonly issue: { + readonly number: number; + readonly opened: boolean; + readonly descriptionEdited: boolean; + readonly commentBody: string; + }; + readonly pullRequest: { + readonly number: number; + readonly action: string; + readonly commentBody: string; + }; + readonly commit: { + readonly commits: readonly unknown[]; + }; + readonly singleAction: { + readonly isThinkAction: boolean; + readonly isRecommendStepsAction: boolean; + readonly isCheckProgressAction: boolean; + readonly isDetectPotentialProblemsAction: boolean; + }; + readonly ai: { + readonly getAiPullRequestDescription: () => boolean; + readonly getAgentConfiguration: (task: AgentTask) => AgentConfiguration | undefined; + }; +} /** Decides whether a route can invoke an agent for its current event. */ -export declare function shouldTrackAgentActivity(execution: Execution, route: AgentActivityRoute): boolean; +export declare function shouldTrackAgentActivity(execution: AgentActivityExecutionContext, route: AgentActivityRoute): boolean; diff --git a/build/cli/src/application/policies/configuration_persistence_policy.d.ts b/build/cli/src/application/policies/configuration_persistence_policy.d.ts index dda27e3c..2c00255d 100644 --- a/build/cli/src/application/policies/configuration_persistence_policy.d.ts +++ b/build/cli/src/application/policies/configuration_persistence_policy.d.ts @@ -1,4 +1,9 @@ -import type { Execution } from '../../data/model/execution'; +export interface ConfigurationPersistenceContext { + readonly isSingleAction: boolean; + readonly singleAction: { + readonly isRecommendStepsAction: boolean; + }; +} /** * Decides whether the completion phase has persistent execution state to save. * @@ -8,4 +13,4 @@ import type { Execution } from '../../data/model/execution'; * Recommendation actions are the exception because they persist their * fingerprint and latest recommendation in the hidden issue configuration. */ -export declare function shouldPersistConfiguration(execution: Pick): boolean; +export declare function shouldPersistConfiguration(execution: ConfigurationPersistenceContext): boolean; diff --git a/build/cli/src/application/policies/deploy_workflow_policy.d.ts b/build/cli/src/application/policies/deploy_workflow_policy.d.ts index 426f222f..4d71af91 100644 --- a/build/cli/src/application/policies/deploy_workflow_policy.d.ts +++ b/build/cli/src/application/policies/deploy_workflow_policy.d.ts @@ -1,4 +1,29 @@ -import type { Execution } from "../../data/model/execution"; +export interface DeployWorkflowExecutionContext { + readonly issue: { + readonly labeled: boolean; + readonly labelAdded: string; + readonly number: number; + readonly title: string; + readonly body: string; + }; + readonly labels: { + readonly deploy: string; + }; + readonly release: { + readonly active: boolean; + readonly branch?: string; + readonly version?: string; + }; + readonly hotfix: { + readonly active: boolean; + readonly branch?: string; + readonly version?: string; + }; + readonly workflows: { + readonly release: string; + readonly hotfix: string; + }; +} export interface DeployWorkflowPlan { kind: "release" | "hotfix"; branch: string; @@ -8,4 +33,4 @@ export interface DeployWorkflowPlan { changelog: string; issue: number; } -export declare function resolveDeployWorkflowPlan(param: Execution): DeployWorkflowPlan | undefined; +export declare function resolveDeployWorkflowPlan(param: DeployWorkflowExecutionContext): DeployWorkflowPlan | undefined; diff --git a/build/cli/src/application/policies/status_command_policy.d.ts b/build/cli/src/application/policies/status_command_policy.d.ts index b6ae9682..a1ebe3c1 100644 --- a/build/cli/src/application/policies/status_command_policy.d.ts +++ b/build/cli/src/application/policies/status_command_policy.d.ts @@ -1,5 +1,41 @@ -import type { Execution } from '../../data/model/execution'; import { Result } from '../../data/model/result'; +import type { CopilotLifecycleLabels } from '../../domain/copilot_lifecycle'; +export interface CopilotStatusExecutionContext { + readonly owner: string; + readonly repo: string; + readonly eventName: string; + readonly issueNumber: number; + readonly isIssue: boolean; + readonly isPush: boolean; + readonly isPullRequest: boolean; + readonly inputs?: { + readonly action?: string; + }; + readonly issue: { + readonly number: number; + }; + readonly pullRequest: { + readonly number: number; + readonly isPullRequestReviewComment: boolean; + }; + readonly commit: { + readonly branch: string; + }; + readonly labels: { + readonly currentIssueLabels?: readonly string[]; + readonly currentPullRequestLabels?: readonly string[]; + readonly lifecycle?: CopilotLifecycleLabels; + }; + readonly currentConfiguration: { + readonly results?: readonly { + readonly payload: unknown; + }[]; + }; + readonly ai: { + readonly getAiPullRequestDescription: () => boolean; + readonly getPullRequestDescriptionMode?: () => string; + }; +} export interface CopilotStatusSnapshot { readonly owner: string; readonly repository: string; @@ -21,6 +57,6 @@ export interface CopilotStatusSnapshot { readonly pullRequestDescriptionMode: string; } /** Builds a read-only status snapshot from the facts already loaded by setup. */ -export declare function buildCopilotStatusSnapshot(execution: Execution): CopilotStatusSnapshot; -export declare function buildCopilotStatusResult(execution: Execution, taskId: string): Result; +export declare function buildCopilotStatusSnapshot(execution: CopilotStatusExecutionContext): CopilotStatusSnapshot; +export declare function buildCopilotStatusResult(execution: CopilotStatusExecutionContext, taskId: string): Result; export declare function formatCopilotStatus(snapshot: CopilotStatusSnapshot): string; diff --git a/build/cli/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts b/build/cli/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts index c213f51a..8f044833 100644 --- a/build/cli/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts +++ b/build/cli/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts @@ -1,10 +1,39 @@ -import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; +import type { ExecutionInputs } from '../../../data/model/execution_inputs'; +import type { CopilotLifecycleLabels } from '../../../domain/copilot_lifecycle'; import type { IssueLabelsPort } from '../../ports/issue_management_ports'; export interface SynchronizeLifecycleStateParam { - execution: Execution; + execution: LifecycleSynchronizationExecution; results: readonly Result[]; } +/** Narrow runtime context required by lifecycle reconciliation. */ +export interface LifecycleSynchronizationExecution { + readonly owner: string; + readonly repo: string; + readonly eventName: string; + readonly inputs: ExecutionInputs | undefined; + readonly issueNumber: number; + readonly isIssue: boolean; + readonly isPullRequest: boolean; + readonly issue: { + readonly number: number; + readonly opened: boolean; + readonly descriptionEdited: boolean; + }; + readonly pullRequest: { + readonly number: number; + readonly isMerged: boolean; + readonly isClosed: boolean; + }; + readonly labels: { + currentIssueLabels: string[]; + currentPullRequestLabels: string[]; + readonly lifecycle: CopilotLifecycleLabels; + }; + readonly tokens: { + readonly token: string; + }; +} /** * Reconciles one state label after a route completes. The existing business * labels remain untouched, and repeated events are idempotent. diff --git a/build/cli/src/data/model/config.d.ts b/build/cli/src/data/model/config.d.ts index 3efd7156..edcbcfbc 100644 --- a/build/cli/src/data/model/config.d.ts +++ b/build/cli/src/data/model/config.d.ts @@ -1,7 +1,20 @@ import { BranchConfiguration } from "./branch_configuration"; import { RecommendationState } from "./recommendation_state"; import { Result } from "./result"; -export declare const CONFIG_SCHEMA_VERSION = 1; +/** Version of the durable configuration contract stored in issue/PR content. */ +export declare const CONFIG_SCHEMA_VERSION = 2; +export interface ConfigurationMigrationResult { + readonly payload: Record; + readonly sourceVersion: number; + readonly migrated: boolean; + readonly futureVersion: boolean; +} +/** + * Normalizes persisted configuration without silently losing fields from a + * newer installation. Unknown keys are deliberately retained so a downgrade + * or a mixed-version workflow can round-trip data safely. + */ +export declare function migrateConfigurationPayload(value: unknown): ConfigurationMigrationResult; export declare class Config { readonly schemaVersion: number; branchType: string; diff --git a/build/cli/src/manager/description/configuration_payload_policy.d.ts b/build/cli/src/manager/description/configuration_payload_policy.d.ts index a0d78d16..0c754944 100644 --- a/build/cli/src/manager/description/configuration_payload_policy.d.ts +++ b/build/cli/src/manager/description/configuration_payload_policy.d.ts @@ -1,2 +1,13 @@ -import type { Execution } from '../../data/model/execution'; -export declare function buildConfigurationPayload(execution: Execution, storedRaw: string | undefined): string; +export interface ConfigurationPayloadContext { + readonly currentConfiguration: { + readonly branchType: string; + readonly releaseBranch?: string; + readonly workingBranch?: string; + readonly parentBranch?: string; + readonly hotfixOriginBranch?: string; + readonly hotfixBranch?: string; + readonly branchConfiguration?: unknown; + readonly recommendationState?: unknown; + }; +} +export declare function buildConfigurationPayload(execution: ConfigurationPayloadContext, storedRaw: string | undefined): string; diff --git a/build/github_action/index.js b/build/github_action/index.js index 0415e74b..5fca6beb 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -52029,15 +52029,16 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.parseAgentCommand = parseAgentCommand; const shellQuote = __importStar(__nccwpck_require__(75430)); +const application_error_1 = __nccwpck_require__(75999); /** Parses a literal agent command without allowing shell operators or substitutions. */ function parseAgentCommand(command) { const trimmed = command.trim(); if (!trimmed) - throw new Error('Agent CLI command must not be empty.'); + throw new application_error_1.ApplicationError('Agent CLI command must not be empty.', 'validation'); const parsed = shellQuote.parse(trimmed, {}); const argv = parsed.filter((entry) => typeof entry === 'string'); if (argv.length !== parsed.length || argv.length === 0) { - throw new Error('Agent CLI command contains unsupported shell syntax. Use an executable and literal arguments only.'); + throw new application_error_1.ApplicationError('Agent CLI command contains unsupported shell syntax. Use an executable and literal arguments only.', 'validation'); } return { executable: argv[0], args: argv.slice(1) }; } @@ -52082,11 +52083,12 @@ function cliInstallationHint(provider) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.validateConfiguredAgentCommand = validateConfiguredAgentCommand; +const application_error_1 = __nccwpck_require__(75999); const agent_command_parser_1 = __nccwpck_require__(15044); function validateConfiguredAgentCommand(configuration) { const command = configuration.command?.trim(); if (!command) - throw new Error(`CLI command is required for ${configuration.provider}.`); + throw new application_error_1.ApplicationError(`CLI command is required for ${configuration.provider}.`, 'validation'); const { args } = (0, agent_command_parser_1.parseAgentCommand)(command); validateCommandShape(configuration, args); validateModelSelection(configuration, args); @@ -52095,13 +52097,13 @@ function validateConfiguredAgentCommand(configuration) { } function validateCommandShape(configuration, args) { if (configuration.provider !== 'codex' && args.includes('-')) { - throw new Error(`${configuration.provider} command must not include the Codex stdin placeholder "-"; its prompt is passed as an argument.`); + throw new application_error_1.ApplicationError(`${configuration.provider} command must not include the Codex stdin placeholder "-"; its prompt is passed as an argument.`, 'validation'); } if (configuration.provider === 'codex' && args.at(-1) !== '-') { - throw new Error('Codex command must end with the stdin placeholder "-".'); + throw new application_error_1.ApplicationError('Codex command must end with the stdin placeholder "-".', 'validation'); } if (!hasFlag(args, '--model') && !hasFlag(args, '-m')) { - throw new Error(`${configuration.provider} command must select the model explicitly with --model.`); + throw new application_error_1.ApplicationError(`${configuration.provider} command must select the model explicitly with --model.`, 'validation'); } } function validateModelSelection(configuration, args) { @@ -52110,18 +52112,18 @@ function validateModelSelection(configuration, args) { : configuration.model.trim(); const configuredModel = flagValue(args, ['--model', '-m']); if (configuredModel !== expectedModel) { - throw new Error(`${configuration.provider} command must select configured model "${expectedModel}".`); + throw new application_error_1.ApplicationError(`${configuration.provider} command must select configured model "${expectedModel}".`, 'validation'); } } function validateProviderConfiguration(configuration, args) { if (configuration.provider !== 'codex') return; if (!hasConfig(args, 'model_provider')) { - throw new Error('Codex command must select the model provider explicitly with --config model_provider=... .'); + throw new application_error_1.ApplicationError('Codex command must select the model provider explicitly with --config model_provider=... .', 'validation'); } const expectedProvider = configuration.modelProvider?.trim() || 'openai'; if (configValue(args, 'model_provider') !== expectedProvider) { - throw new Error(`Codex command must select configured model provider "${expectedProvider}".`); + throw new application_error_1.ApplicationError(`Codex command must select configured model provider "${expectedProvider}".`, 'validation'); } } function validateEffortSelection(configuration, args) { @@ -52130,10 +52132,10 @@ function validateEffortSelection(configuration, args) { return; if (configuration.provider === 'codex') { if (!hasConfig(args, 'model_reasoning_effort')) { - throw new Error('Codex command must select effort explicitly with --config model_reasoning_effort=... .'); + throw new application_error_1.ApplicationError('Codex command must select effort explicitly with --config model_reasoning_effort=... .', 'validation'); } if (configValue(args, 'model_reasoning_effort') !== effort) { - throw new Error(`Codex command must select configured effort "${effort}".`); + throw new application_error_1.ApplicationError(`Codex command must select configured effort "${effort}".`, 'validation'); } return; } @@ -52145,10 +52147,10 @@ function validateEffortSelection(configuration, args) { return; } if (!hasFlag(args, '--variant')) { - throw new Error('OpenCode command must select effort explicitly with --variant ... .'); + throw new application_error_1.ApplicationError('OpenCode command must select effort explicitly with --variant ... .', 'validation'); } if (flagValue(args, ['--variant']) !== effort) { - throw new Error(`OpenCode command must select configured effort "${effort}".`); + throw new application_error_1.ApplicationError(`OpenCode command must select configured effort "${effort}".`, 'validation'); } } function hasFlag(args, flag) { @@ -52238,7 +52240,7 @@ function hasTaskOverride(value) { /***/ }), /***/ 60596: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -52249,11 +52251,12 @@ exports.resolveModelProvider = resolveModelProvider; exports.resolveModel = resolveModel; exports.resolveEffort = resolveEffort; exports.assertModelAllowlisted = assertModelAllowlisted; +const application_error_1 = __nccwpck_require__(75999); exports.SUPPORTED_AGENT_PROVIDERS = ['opencode', 'cursor', 'codex']; function resolveAgentProvider(value) { if (exports.SUPPORTED_AGENT_PROVIDERS.includes(value)) return value; - throw new Error(`Unsupported agent provider "${value}". Supported providers: ${exports.SUPPORTED_AGENT_PROVIDERS.join(', ')}.`); + throw new application_error_1.ApplicationError(`Unsupported agent provider "${value}". Supported providers: ${exports.SUPPORTED_AGENT_PROVIDERS.join(', ')}.`, 'validation'); } function resolveModelProvider(value, environment) { const provider = value?.trim().toLowerCase() || 'openai'; @@ -52264,7 +52267,7 @@ function resolveModelProvider(value, environment) { function resolveModel(value) { const model = value.trim(); if (!model) - throw new Error('Agent model must not be empty.'); + throw new application_error_1.ApplicationError('Agent model must not be empty.', 'validation'); assertIdentifier(model, 'Agent model must be a simple model identifier without whitespace or shell syntax.', /^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/); return model; } @@ -52277,25 +52280,25 @@ function resolveEffort(value) { function assertModelAllowlisted(modelProvider, model, environment) { const allowedModels = parseAllowlist(environment.AGENT_ALLOWED_MODELS); if (allowedModels.length > 0 && !allowedModels.includes(`${modelProvider}/${model}`) && !allowedModels.includes(model)) { - throw new Error(`Agent model "${modelProvider}/${model}" is not allowlisted.`); + throw new application_error_1.ApplicationError(`Agent model "${modelProvider}/${model}" is not allowlisted.`, 'authorization'); } } function assertAllowlisted(name, value, environment) { const values = parseAllowlist(environment[name]); if (values.length > 0 && !values.includes(value)) - throw new Error(`Agent model provider "${value}" is not allowlisted.`); + throw new application_error_1.ApplicationError(`Agent model provider "${value}" is not allowlisted.`, 'authorization'); } function parseAllowlist(raw) { if (!raw?.trim()) return []; const values = raw.split(',').map(value => value.trim().toLowerCase()).filter(Boolean); if (values.length === 0) - throw new Error('Agent allowlist must contain at least one value.'); + throw new application_error_1.ApplicationError('Agent allowlist must contain at least one value.', 'configuration'); return values; } function assertIdentifier(value, message, pattern = /^[a-z0-9][a-z0-9_-]*$/i) { if (!pattern.test(value)) - throw new Error(message); + throw new application_error_1.ApplicationError(message, 'validation'); } @@ -54183,6 +54186,7 @@ exports.validateReleaseInput = validateReleaseInput; exports.normalizeVersion = normalizeVersion; exports.versionForRelease = versionForRelease; const constants_1 = __nccwpck_require__(15415); +const application_error_1 = __nccwpck_require__(75999); const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/; function validateReleaseInput(input) { if (!input.version.length) @@ -54203,7 +54207,7 @@ function normalizeVersion(version) { function versionForRelease(version) { const normalized = normalizeVersion(version); if (normalized === undefined) - throw new Error('Cannot build a release version from invalid input.'); + throw new application_error_1.ApplicationError('Cannot build a release version from invalid input.', 'validation'); return `v${normalized}`; } @@ -56002,6 +56006,7 @@ async function resolveExecutionIssueNumber(execution, issueRepository) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ResolveGithubExecutionAdmissionUseCase = void 0; +const application_error_1 = __nccwpck_require__(75999); const github_execution_admission_policy_1 = __nccwpck_require__(80765); class ResolveGithubExecutionAdmissionUseCase { constructor(authenticatedUserPort) { @@ -56011,7 +56016,7 @@ class ResolveGithubExecutionAdmissionUseCase { async invoke(request) { const tokenUser = await this.authenticatedUserPort.getUserFromToken(request.token); if (typeof tokenUser !== 'string' || tokenUser.trim().length === 0) { - throw new Error('Failed to get user from token'); + throw new application_error_1.ApplicationError('Failed to get user from token', 'authorization'); } return { tokenUser, @@ -56066,6 +56071,7 @@ exports.SetupExecutionUseCase = SetupExecutionUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runSetupExecution = runSetupExecution; +const application_error_1 = __nccwpck_require__(75999); const initial_labels_policy_1 = __nccwpck_require__(50293); const previous_branch_state_policy_1 = __nccwpck_require__(43630); const logging_ports_1 = __nccwpck_require__(6152); @@ -56093,7 +56099,7 @@ async function loadTokenUser(execution, organizationSetupPort) { return; execution.tokenUser = await organizationSetupPort.getUserFromToken(execution.tokens.token); if (!execution.tokenUser) - throw new Error('Failed to get user from token'); + throw new application_error_1.ApplicationError('Failed to get user from token', 'authorization'); } async function loadPreviousConfiguration(execution, configurationPort) { const issueNumber = configurationIssueNumber(execution); @@ -56646,6 +56652,7 @@ async function runUserRequestCommitAndPush(execution, options, authenticatedUser Object.defineProperty(exports, "__esModule", ({ value: true })); exports.finalizeBugbotAutofix = finalizeBugbotAutofix; const result_1 = __nccwpck_require__(73817); +const application_error_1 = __nccwpck_require__(75999); const workspace_changes_1 = __nccwpck_require__(93370); const logging_ports_1 = __nccwpck_require__(6152); async function finalizeBugbotAutofix(execution, context, idsToFix, workspacePathsBefore, responseText, gitCommitPort) { @@ -56678,7 +56685,7 @@ async function inspectWorkspace(gitCommitPort, phase) { return await (0, workspace_changes_1.listWorkspacePaths)(gitCommitPort); } catch (error) { - throw new Error(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`); + throw new application_error_1.ApplicationError(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`, 'provider', { cause: error, retryable: true }); } } function failure(message) { @@ -56696,6 +56703,7 @@ function failure(message) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prepareBugbotAutofix = prepareBugbotAutofix; const result_1 = __nccwpck_require__(73817); +const application_error_1 = __nccwpck_require__(75999); const types_1 = __nccwpck_require__(32632); const build_bugbot_fix_prompt_1 = __nccwpck_require__(89819); const load_bugbot_context_use_case_1 = __nccwpck_require__(4050); @@ -56729,7 +56737,7 @@ async function inspectWorkspace(gitCommitPort, phase) { return await (0, workspace_changes_1.listWorkspacePaths)(gitCommitPort); } catch (error) { - throw new Error(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`); + throw new application_error_1.ApplicationError(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`, 'provider', { cause: error, retryable: true }); } } function failure(message) { @@ -58101,6 +58109,7 @@ exports.replaceMarkerInBody = replaceMarkerInBody; exports.extractTitleFromBody = extractTitleFromBody; exports.buildCommentBody = buildCommentBody; const constants_1 = __nccwpck_require__(15415); +const application_error_1 = __nccwpck_require__(75999); const github_comment_publication_policy_1 = __nccwpck_require__(72712); /** Maximum lossless finding identity accepted by the marker contract. */ exports.MAX_FINDING_ID_LENGTH = 200; @@ -58124,11 +58133,11 @@ function normalizeFindingIdForMarker(findingId) { function requireFindingIdForMarker(findingId) { const safeId = normalizeFindingIdForMarker(findingId); if (safeId == null) { - throw new Error(findingId.trim().length === 0 + throw new application_error_1.ApplicationError(findingId.trim().length === 0 ? "Finding ID is empty after marker sanitization." : findingId.trim().length > exports.MAX_FINDING_ID_LENGTH ? "Finding ID exceeds the maximum marker length." - : "Finding ID contains marker-breaking characters."); + : "Finding ID contains marker-breaking characters.", 'validation'); } return safeId; } @@ -60322,6 +60331,7 @@ function shouldRenderImage(param, image) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.StoreConfigurationUseCase = void 0; +const application_error_1 = __nccwpck_require__(75999); const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); /** @@ -60339,7 +60349,7 @@ class StoreConfigurationUseCase { } catch (error) { (0, logging_ports_1.logError)(`StoreConfiguration: failed to update configuration.`, error instanceof Error ? { stack: error.stack } : undefined); - throw new Error('Configuration persistence failed.'); + throw new application_error_1.ApplicationError('Configuration persistence failed.', 'provider', { cause: error, retryable: true }); } } } @@ -62553,6 +62563,7 @@ exports.CheckPullRequestCommentLanguageUseCase = CheckPullRequestCommentLanguage Object.defineProperty(exports, "__esModule", ({ value: true })); exports.WaitForPreviousWorkflowRunsUseCase = void 0; const workflow_queue_policy_1 = __nccwpck_require__(43193); +const application_error_1 = __nccwpck_require__(75999); const SYSTEM_CLOCK = { nowMilliseconds: () => Date.now() }; const SYSTEM_RANDOM = { next: () => Math.random() }; class WaitForPreviousWorkflowRunsUseCase { @@ -62570,13 +62581,13 @@ class WaitForPreviousWorkflowRunsUseCase { let pollIndex = 0; while (true) { if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } const activeRunCount = await this.queryPort.countActivePreviousRuns(query, { deadlineAtMilliseconds, }); if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } if (activeRunCount === 0) { this.observerPort.noActivePreviousRuns(); @@ -62584,7 +62595,7 @@ class WaitForPreviousWorkflowRunsUseCase { } const delayMilliseconds = (0, workflow_queue_policy_1.calculateWorkflowPollingDelay)(pollIndex, this.random.next(), this.policy); if (this.clock.nowMilliseconds() + delayMilliseconds >= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } this.observerPort.waitingForPreviousRuns(activeRunCount, delayMilliseconds); await this.delayPort.wait(delayMilliseconds); @@ -62593,6 +62604,9 @@ class WaitForPreviousWorkflowRunsUseCase { } } exports.WaitForPreviousWorkflowRunsUseCase = WaitForPreviousWorkflowRunsUseCase; +function queueTimeoutError() { + return new application_error_1.ApplicationError('Timeout waiting for previous runs to finish.', 'workflow', { retryable: true }); +} /***/ }), @@ -62806,19 +62820,51 @@ exports.Commit = Commit; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Config = exports.CONFIG_SCHEMA_VERSION = void 0; +exports.migrateConfigurationPayload = migrateConfigurationPayload; const branch_configuration_1 = __nccwpck_require__(71934); const recommendation_state_1 = __nccwpck_require__(68514); const model_input_1 = __nccwpck_require__(14637); -exports.CONFIG_SCHEMA_VERSION = 1; +/** Version of the durable configuration contract stored in issue/PR content. */ +exports.CONFIG_SCHEMA_VERSION = 2; +/** + * Normalizes persisted configuration without silently losing fields from a + * newer installation. Unknown keys are deliberately retained so a downgrade + * or a mixed-version workflow can round-trip data safely. + */ +function migrateConfigurationPayload(value) { + const original = { ...(0, model_input_1.asModelInput)(value) }; + const sourceVersion = readSchemaVersion(original['schemaVersion']); + if (sourceVersion > exports.CONFIG_SCHEMA_VERSION) { + return { + payload: original, + sourceVersion, + migrated: false, + futureVersion: true, + }; + } + const payload = { ...original }; + const hadTransientResults = Object.prototype.hasOwnProperty.call(payload, 'results'); + delete payload.results; + if (payload.branchConfiguration === null) + delete payload.branchConfiguration; + if (!(0, recommendation_state_1.isRecommendationState)(payload.recommendationState)) + delete payload.recommendationState; + payload.schemaVersion = exports.CONFIG_SCHEMA_VERSION; + return { + payload, + sourceVersion, + migrated: sourceVersion !== exports.CONFIG_SCHEMA_VERSION || hadTransientResults, + futureVersion: false, + }; +} +function readSchemaVersion(value) { + return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : 0; +} class Config { constructor(data) { this.results = []; - const input = (0, model_input_1.asModelInput)(data); - this.schemaVersion = typeof input.schemaVersion === 'number' - && Number.isInteger(input.schemaVersion) - && input.schemaVersion > 0 - ? input.schemaVersion - : exports.CONFIG_SCHEMA_VERSION; + const input = (0, model_input_1.asModelInput)(migrateConfigurationPayload(data).payload); + this.schemaVersion = readSchemaVersion(input.schemaVersion) || exports.CONFIG_SCHEMA_VERSION; this.branchType = (0, model_input_1.readString)(input, 'branchType'); this.hotfixOriginBranch = (0, model_input_1.readOptionalString)(input, 'hotfixOriginBranch'); this.hotfixBranch = (0, model_input_1.readOptionalString)(input, 'hotfixBranch'); @@ -71423,6 +71469,7 @@ exports.buildConfigurationPayload = buildConfigurationPayload; const config_1 = __nccwpck_require__(90450); function buildConfigurationPayload(execution, storedRaw) { const current = execution.currentConfiguration; + const stored = parseStoredConfiguration(storedRaw); const payload = { schemaVersion: config_1.CONFIG_SCHEMA_VERSION, branchType: current.branchType, @@ -71434,7 +71481,8 @@ function buildConfigurationPayload(execution, storedRaw) { branchConfiguration: current.branchConfiguration, recommendationState: current.recommendationState, }; - mergeMissingValues(payload, parseStoredConfiguration(storedRaw)); + mergeMissingValues(payload, stored); + preserveFutureSchemaVersion(payload, stored); delete payload.results; return JSON.stringify(payload, null, 4); } @@ -71442,7 +71490,7 @@ function parseStoredConfiguration(storedRaw) { if (!storedRaw?.trim()) return undefined; try { - return JSON.parse(storedRaw); + return (0, config_1.migrateConfigurationPayload)(JSON.parse(storedRaw)).payload; } catch { return undefined; @@ -71456,6 +71504,11 @@ function mergeMissingValues(payload, stored) { payload[key] = stored[key]; } } +function preserveFutureSchemaVersion(payload, stored) { + if (typeof stored?.schemaVersion === 'number' && stored.schemaVersion > config_1.CONFIG_SCHEMA_VERSION) { + payload.schemaVersion = stored.schemaVersion; + } +} /***/ }), diff --git a/build/github_action/src/application/policies/agent_activity_policy.d.ts b/build/github_action/src/application/policies/agent_activity_policy.d.ts index b26fe59f..01511480 100644 --- a/build/github_action/src/application/policies/agent_activity_policy.d.ts +++ b/build/github_action/src/application/policies/agent_activity_policy.d.ts @@ -1,4 +1,32 @@ -import type { Execution } from '../../data/model/execution'; +import type { AgentConfiguration, AgentTask } from '../../domain/agent'; export type AgentActivityRoute = 'single-action' | 'issue-comment' | 'issue' | 'pull-request-review-comment' | 'pull-request' | 'push'; +export interface AgentActivityExecutionContext { + readonly eventName: string; + readonly issueNumber: number; + readonly issue: { + readonly number: number; + readonly opened: boolean; + readonly descriptionEdited: boolean; + readonly commentBody: string; + }; + readonly pullRequest: { + readonly number: number; + readonly action: string; + readonly commentBody: string; + }; + readonly commit: { + readonly commits: readonly unknown[]; + }; + readonly singleAction: { + readonly isThinkAction: boolean; + readonly isRecommendStepsAction: boolean; + readonly isCheckProgressAction: boolean; + readonly isDetectPotentialProblemsAction: boolean; + }; + readonly ai: { + readonly getAiPullRequestDescription: () => boolean; + readonly getAgentConfiguration: (task: AgentTask) => AgentConfiguration | undefined; + }; +} /** Decides whether a route can invoke an agent for its current event. */ -export declare function shouldTrackAgentActivity(execution: Execution, route: AgentActivityRoute): boolean; +export declare function shouldTrackAgentActivity(execution: AgentActivityExecutionContext, route: AgentActivityRoute): boolean; diff --git a/build/github_action/src/application/policies/configuration_persistence_policy.d.ts b/build/github_action/src/application/policies/configuration_persistence_policy.d.ts index dda27e3c..2c00255d 100644 --- a/build/github_action/src/application/policies/configuration_persistence_policy.d.ts +++ b/build/github_action/src/application/policies/configuration_persistence_policy.d.ts @@ -1,4 +1,9 @@ -import type { Execution } from '../../data/model/execution'; +export interface ConfigurationPersistenceContext { + readonly isSingleAction: boolean; + readonly singleAction: { + readonly isRecommendStepsAction: boolean; + }; +} /** * Decides whether the completion phase has persistent execution state to save. * @@ -8,4 +13,4 @@ import type { Execution } from '../../data/model/execution'; * Recommendation actions are the exception because they persist their * fingerprint and latest recommendation in the hidden issue configuration. */ -export declare function shouldPersistConfiguration(execution: Pick): boolean; +export declare function shouldPersistConfiguration(execution: ConfigurationPersistenceContext): boolean; diff --git a/build/github_action/src/application/policies/deploy_workflow_policy.d.ts b/build/github_action/src/application/policies/deploy_workflow_policy.d.ts index 426f222f..4d71af91 100644 --- a/build/github_action/src/application/policies/deploy_workflow_policy.d.ts +++ b/build/github_action/src/application/policies/deploy_workflow_policy.d.ts @@ -1,4 +1,29 @@ -import type { Execution } from "../../data/model/execution"; +export interface DeployWorkflowExecutionContext { + readonly issue: { + readonly labeled: boolean; + readonly labelAdded: string; + readonly number: number; + readonly title: string; + readonly body: string; + }; + readonly labels: { + readonly deploy: string; + }; + readonly release: { + readonly active: boolean; + readonly branch?: string; + readonly version?: string; + }; + readonly hotfix: { + readonly active: boolean; + readonly branch?: string; + readonly version?: string; + }; + readonly workflows: { + readonly release: string; + readonly hotfix: string; + }; +} export interface DeployWorkflowPlan { kind: "release" | "hotfix"; branch: string; @@ -8,4 +33,4 @@ export interface DeployWorkflowPlan { changelog: string; issue: number; } -export declare function resolveDeployWorkflowPlan(param: Execution): DeployWorkflowPlan | undefined; +export declare function resolveDeployWorkflowPlan(param: DeployWorkflowExecutionContext): DeployWorkflowPlan | undefined; diff --git a/build/github_action/src/application/policies/status_command_policy.d.ts b/build/github_action/src/application/policies/status_command_policy.d.ts index b6ae9682..a1ebe3c1 100644 --- a/build/github_action/src/application/policies/status_command_policy.d.ts +++ b/build/github_action/src/application/policies/status_command_policy.d.ts @@ -1,5 +1,41 @@ -import type { Execution } from '../../data/model/execution'; import { Result } from '../../data/model/result'; +import type { CopilotLifecycleLabels } from '../../domain/copilot_lifecycle'; +export interface CopilotStatusExecutionContext { + readonly owner: string; + readonly repo: string; + readonly eventName: string; + readonly issueNumber: number; + readonly isIssue: boolean; + readonly isPush: boolean; + readonly isPullRequest: boolean; + readonly inputs?: { + readonly action?: string; + }; + readonly issue: { + readonly number: number; + }; + readonly pullRequest: { + readonly number: number; + readonly isPullRequestReviewComment: boolean; + }; + readonly commit: { + readonly branch: string; + }; + readonly labels: { + readonly currentIssueLabels?: readonly string[]; + readonly currentPullRequestLabels?: readonly string[]; + readonly lifecycle?: CopilotLifecycleLabels; + }; + readonly currentConfiguration: { + readonly results?: readonly { + readonly payload: unknown; + }[]; + }; + readonly ai: { + readonly getAiPullRequestDescription: () => boolean; + readonly getPullRequestDescriptionMode?: () => string; + }; +} export interface CopilotStatusSnapshot { readonly owner: string; readonly repository: string; @@ -21,6 +57,6 @@ export interface CopilotStatusSnapshot { readonly pullRequestDescriptionMode: string; } /** Builds a read-only status snapshot from the facts already loaded by setup. */ -export declare function buildCopilotStatusSnapshot(execution: Execution): CopilotStatusSnapshot; -export declare function buildCopilotStatusResult(execution: Execution, taskId: string): Result; +export declare function buildCopilotStatusSnapshot(execution: CopilotStatusExecutionContext): CopilotStatusSnapshot; +export declare function buildCopilotStatusResult(execution: CopilotStatusExecutionContext, taskId: string): Result; export declare function formatCopilotStatus(snapshot: CopilotStatusSnapshot): string; diff --git a/build/github_action/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts b/build/github_action/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts index c213f51a..8f044833 100644 --- a/build/github_action/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts +++ b/build/github_action/src/application/usecases/actions/synchronize_lifecycle_state_use_case.d.ts @@ -1,10 +1,39 @@ -import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; +import type { ExecutionInputs } from '../../../data/model/execution_inputs'; +import type { CopilotLifecycleLabels } from '../../../domain/copilot_lifecycle'; import type { IssueLabelsPort } from '../../ports/issue_management_ports'; export interface SynchronizeLifecycleStateParam { - execution: Execution; + execution: LifecycleSynchronizationExecution; results: readonly Result[]; } +/** Narrow runtime context required by lifecycle reconciliation. */ +export interface LifecycleSynchronizationExecution { + readonly owner: string; + readonly repo: string; + readonly eventName: string; + readonly inputs: ExecutionInputs | undefined; + readonly issueNumber: number; + readonly isIssue: boolean; + readonly isPullRequest: boolean; + readonly issue: { + readonly number: number; + readonly opened: boolean; + readonly descriptionEdited: boolean; + }; + readonly pullRequest: { + readonly number: number; + readonly isMerged: boolean; + readonly isClosed: boolean; + }; + readonly labels: { + currentIssueLabels: string[]; + currentPullRequestLabels: string[]; + readonly lifecycle: CopilotLifecycleLabels; + }; + readonly tokens: { + readonly token: string; + }; +} /** * Reconciles one state label after a route completes. The existing business * labels remain untouched, and repeated events are idempotent. diff --git a/build/github_action/src/data/model/config.d.ts b/build/github_action/src/data/model/config.d.ts index 3efd7156..edcbcfbc 100644 --- a/build/github_action/src/data/model/config.d.ts +++ b/build/github_action/src/data/model/config.d.ts @@ -1,7 +1,20 @@ import { BranchConfiguration } from "./branch_configuration"; import { RecommendationState } from "./recommendation_state"; import { Result } from "./result"; -export declare const CONFIG_SCHEMA_VERSION = 1; +/** Version of the durable configuration contract stored in issue/PR content. */ +export declare const CONFIG_SCHEMA_VERSION = 2; +export interface ConfigurationMigrationResult { + readonly payload: Record; + readonly sourceVersion: number; + readonly migrated: boolean; + readonly futureVersion: boolean; +} +/** + * Normalizes persisted configuration without silently losing fields from a + * newer installation. Unknown keys are deliberately retained so a downgrade + * or a mixed-version workflow can round-trip data safely. + */ +export declare function migrateConfigurationPayload(value: unknown): ConfigurationMigrationResult; export declare class Config { readonly schemaVersion: number; branchType: string; diff --git a/build/github_action/src/manager/description/configuration_payload_policy.d.ts b/build/github_action/src/manager/description/configuration_payload_policy.d.ts index a0d78d16..0c754944 100644 --- a/build/github_action/src/manager/description/configuration_payload_policy.d.ts +++ b/build/github_action/src/manager/description/configuration_payload_policy.d.ts @@ -1,2 +1,13 @@ -import type { Execution } from '../../data/model/execution'; -export declare function buildConfigurationPayload(execution: Execution, storedRaw: string | undefined): string; +export interface ConfigurationPayloadContext { + readonly currentConfiguration: { + readonly branchType: string; + readonly releaseBranch?: string; + readonly workingBranch?: string; + readonly parentBranch?: string; + readonly hotfixOriginBranch?: string; + readonly hotfixBranch?: string; + readonly branchConfiguration?: unknown; + readonly recommendationState?: unknown; + }; +} +export declare function buildConfigurationPayload(execution: ConfigurationPayloadContext, storedRaw: string | undefined): string; diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index 42f83ffb..a44118a5 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -24,6 +24,19 @@ temporary health-workflow bootstrap are infrastructure adapters. Secret values never enter setup override files, Variables, logs, or the generated workflow templates. +Runtime aggregates are kept at the composition boundary. Extracted policies and +reconciliation use cases receive narrow context contracts containing only the +facts they need; they do not depend on the complete `Execution` aggregate. +This keeps pure decisions independently testable and makes new event sources +less likely to couple unrelated capabilities. Application failures use the +typed `ApplicationError` contract with a category, retry hint, and diagnostic +cause; the cause is never published as user-facing workflow output. + +Configuration embedded in issue or pull-request content is versioned. Readers +migrate legacy payloads, remove transient execution data, retain unknown keys, +and preserve future schema versions so an older workflow cannot silently +downgrade configuration written by a newer one. + ## Workflow queue boundary The repository-wide mutation queue is an application use case backed by semantic diff --git a/docs/development/testing.mdx b/docs/development/testing.mdx index 1ef73921..b0d178ed 100644 --- a/docs/development/testing.mdx +++ b/docs/development/testing.mdx @@ -33,6 +33,17 @@ discard, valid single-action preservation, identity lookup failure, and the abse of project composition, agent provisioning, setup, publication, and persistence on discarded runs. +The lifecycle replay integration suite feeds normalized, serialized-shaped +`pull_request_review`, `check_suite`, and `workflow_run` payloads through the +real lifecycle synchronization use case. It verifies deterministic labels for +approved reviews, requested changes, pending checks, failed checks, successful +checks, and ambiguous multi-PR events without invoking an agent route. + +Configuration migration tests cover legacy payloads, transient `results`, +malformed durable values, and future-schema preservation. Architecture tests +also guard the narrow context contracts used by extracted policies and lifecycle +reconciliation. + Architecture tests additionally verify that production imports remain acyclic, application code does not depend on concrete adapters, and pure model/policy code remains free of runtime, provider, and logging dependencies. Refresh the diff --git a/scripts/collect-architecture-metrics.cjs b/scripts/collect-architecture-metrics.cjs index f2a056e0..d2e25535 100644 --- a/scripts/collect-architecture-metrics.cjs +++ b/scripts/collect-architecture-metrics.cjs @@ -310,7 +310,8 @@ function parseLcovInventory(content, repositoryRoot) { function commandTimeout(command) { const executable = path.basename(command[0]); - if (executable === "pnpm" && command.includes("jest")) return 600_000; + if ((executable === "pnpm" && command.includes("jest")) + || (executable === "jest" && command.includes("--coverage"))) return 600_000; if (executable.includes("repowise") && command[1] === "init") return 900_000; if (executable.includes("repowise")) return 300_000; if (executable.includes("graphify")) return 600_000; diff --git a/src/application/__tests__/architecture_boundaries.test.ts b/src/application/__tests__/architecture_boundaries.test.ts index 7f31a977..b099d2ad 100644 --- a/src/application/__tests__/architecture_boundaries.test.ts +++ b/src/application/__tests__/architecture_boundaries.test.ts @@ -133,6 +133,30 @@ describe('application architecture boundaries', () => { const portSource = readFileSync(join(__dirname, '../ports/execution_configuration_ports.ts'), 'utf8'); expect(portSource).not.toContain("data/model/execution"); }); + + it('keeps extracted application policies independent from the complete Execution aggregate', () => { + const policyFiles = [ + 'agent_activity_policy.ts', + 'configuration_persistence_policy.ts', + 'deploy_workflow_policy.ts', + 'status_command_policy.ts', + ]; + const violations = policyFiles + .map(file => ({ file, source: readFileSync(join(applicationRoot, 'policies', file), 'utf8') })) + .filter(({ source }) => /data\/model\/execution(?:['"]|\b)/.test(source)) + .map(({ file }) => file); + + expect(violations).toEqual([]); + }); + + it('keeps lifecycle reconciliation dependent on a narrow context contract', () => { + const source = readFileSync( + join(applicationRoot, 'usecases/actions/synchronize_lifecycle_state_use_case.ts'), + 'utf8', + ); + expect(source).not.toContain("data/model/execution'"); + expect(source).toContain('LifecycleSynchronizationExecution'); + }); }); describe('failure policy ownership', () => { diff --git a/src/application/errors/__tests__/application_error.test.ts b/src/application/errors/__tests__/application_error.test.ts new file mode 100644 index 00000000..15a42b96 --- /dev/null +++ b/src/application/errors/__tests__/application_error.test.ts @@ -0,0 +1,31 @@ +import { ApplicationError, toApplicationError } from '../application_error'; + +describe('ApplicationError', () => { + it('exposes a stable semantic contract for callers', () => { + const cause = new Error('network unavailable'); + const error = new ApplicationError('Unable to reach provider.', 'provider', { + cause, + retryable: true, + }); + + expect(error).toMatchObject({ + name: 'ApplicationError', + kind: 'provider', + retryable: true, + cause, + }); + }); + + it('preserves application errors and normalizes unknown failures', () => { + const existing = new ApplicationError('invalid setup', 'validation'); + const original = new Error('unexpected'); + + expect(toApplicationError(existing, 'ignored', 'unknown')).toBe(existing); + expect(toApplicationError(original, 'Operation failed.', 'workflow', { retryable: true })).toMatchObject({ + message: 'Operation failed.', + kind: 'workflow', + retryable: true, + cause: original, + }); + }); +}); diff --git a/src/application/policies/agent_activity_policy.ts b/src/application/policies/agent_activity_policy.ts index 996c4634..28a38a75 100644 --- a/src/application/policies/agent_activity_policy.ts +++ b/src/application/policies/agent_activity_policy.ts @@ -1,4 +1,4 @@ -import type { Execution } from '../../data/model/execution'; +import type { AgentConfiguration, AgentTask } from '../../domain/agent'; import { isAgentConfigurationReady } from '../../domain/agent'; export type AgentActivityRoute = @@ -9,9 +9,38 @@ export type AgentActivityRoute = | 'pull-request' | 'push'; +export interface AgentActivityExecutionContext { + readonly eventName: string; + readonly issueNumber: number; + readonly issue: { + readonly number: number; + readonly opened: boolean; + readonly descriptionEdited: boolean; + readonly commentBody: string; + }; + readonly pullRequest: { + readonly number: number; + readonly action: string; + readonly commentBody: string; + }; + readonly commit: { + readonly commits: readonly unknown[]; + }; + readonly singleAction: { + readonly isThinkAction: boolean; + readonly isRecommendStepsAction: boolean; + readonly isCheckProgressAction: boolean; + readonly isDetectPotentialProblemsAction: boolean; + }; + readonly ai: { + readonly getAiPullRequestDescription: () => boolean; + readonly getAgentConfiguration: (task: AgentTask) => AgentConfiguration | undefined; + }; +} + /** Decides whether a route can invoke an agent for its current event. */ export function shouldTrackAgentActivity( - execution: Execution, + execution: AgentActivityExecutionContext, route: AgentActivityRoute, ): boolean { if (!hasTarget(execution)) return false; @@ -39,7 +68,7 @@ export function shouldTrackAgentActivity( } } -function isAgentBackedSingleAction(execution: Execution): boolean { +function isAgentBackedSingleAction(execution: AgentActivityExecutionContext): boolean { if (execution.singleAction.isThinkAction || execution.singleAction.isRecommendStepsAction) { return isAgentReady(execution, 'planner'); } @@ -49,15 +78,15 @@ function isAgentBackedSingleAction(execution: Execution): boolean { return false; } -function isAgentReady(execution: Execution, task: Parameters[0]): boolean { +function isAgentReady(execution: AgentActivityExecutionContext, task: AgentTask): boolean { return isAgentConfigurationReady(execution.ai?.getAgentConfiguration(task)); } -function hasComment(execution: Execution): boolean { +function hasComment(execution: AgentActivityExecutionContext): boolean { return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; } -function hasTarget(execution: Execution): boolean { +function hasTarget(execution: AgentActivityExecutionContext): boolean { if (['pull_request', 'pull_request_review', 'pull_request_review_comment', 'check_suite', 'workflow_run'].includes(execution.eventName)) { return execution.pullRequest.number > 0; } diff --git a/src/application/policies/agent_command_parser.ts b/src/application/policies/agent_command_parser.ts index 57221a74..78068fe4 100644 --- a/src/application/policies/agent_command_parser.ts +++ b/src/application/policies/agent_command_parser.ts @@ -1,4 +1,5 @@ import * as shellQuote from 'shell-quote'; +import { ApplicationError } from '../errors/application_error'; export interface ParsedAgentCommand { executable: string; @@ -8,11 +9,14 @@ export interface ParsedAgentCommand { /** Parses a literal agent command without allowing shell operators or substitutions. */ export function parseAgentCommand(command: string): ParsedAgentCommand { const trimmed = command.trim(); - if (!trimmed) throw new Error('Agent CLI command must not be empty.'); + if (!trimmed) throw new ApplicationError('Agent CLI command must not be empty.', 'validation'); const parsed = shellQuote.parse(trimmed, {}); const argv = parsed.filter((entry): entry is string => typeof entry === 'string'); if (argv.length !== parsed.length || argv.length === 0) { - throw new Error('Agent CLI command contains unsupported shell syntax. Use an executable and literal arguments only.'); + throw new ApplicationError( + 'Agent CLI command contains unsupported shell syntax. Use an executable and literal arguments only.', + 'validation', + ); } return { executable: argv[0], args: argv.slice(1) }; } diff --git a/src/application/policies/agent_command_validation_policy.ts b/src/application/policies/agent_command_validation_policy.ts index 2cbf2d9a..71accb10 100644 --- a/src/application/policies/agent_command_validation_policy.ts +++ b/src/application/policies/agent_command_validation_policy.ts @@ -1,9 +1,10 @@ import type { AgentConfiguration } from '../../domain/agent'; +import { ApplicationError } from '../errors/application_error'; import { parseAgentCommand } from './agent_command_parser'; export function validateConfiguredAgentCommand(configuration: AgentConfiguration): void { const command = configuration.command?.trim(); - if (!command) throw new Error(`CLI command is required for ${configuration.provider}.`); + if (!command) throw new ApplicationError(`CLI command is required for ${configuration.provider}.`, 'validation'); const { args } = parseAgentCommand(command); validateCommandShape(configuration, args); validateModelSelection(configuration, args); @@ -13,13 +14,13 @@ export function validateConfiguredAgentCommand(configuration: AgentConfiguration function validateCommandShape(configuration: AgentConfiguration, args: readonly string[]): void { if (configuration.provider !== 'codex' && args.includes('-')) { - throw new Error(`${configuration.provider} command must not include the Codex stdin placeholder "-"; its prompt is passed as an argument.`); + throw new ApplicationError(`${configuration.provider} command must not include the Codex stdin placeholder "-"; its prompt is passed as an argument.`, 'validation'); } if (configuration.provider === 'codex' && args.at(-1) !== '-') { - throw new Error('Codex command must end with the stdin placeholder "-".'); + throw new ApplicationError('Codex command must end with the stdin placeholder "-".', 'validation'); } if (!hasFlag(args, '--model') && !hasFlag(args, '-m')) { - throw new Error(`${configuration.provider} command must select the model explicitly with --model.`); + throw new ApplicationError(`${configuration.provider} command must select the model explicitly with --model.`, 'validation'); } } @@ -29,18 +30,18 @@ function validateModelSelection(configuration: AgentConfiguration, args: readonl : configuration.model.trim(); const configuredModel = flagValue(args, ['--model', '-m']); if (configuredModel !== expectedModel) { - throw new Error(`${configuration.provider} command must select configured model "${expectedModel}".`); + throw new ApplicationError(`${configuration.provider} command must select configured model "${expectedModel}".`, 'validation'); } } function validateProviderConfiguration(configuration: AgentConfiguration, args: readonly string[]): void { if (configuration.provider !== 'codex') return; if (!hasConfig(args, 'model_provider')) { - throw new Error('Codex command must select the model provider explicitly with --config model_provider=... .'); + throw new ApplicationError('Codex command must select the model provider explicitly with --config model_provider=... .', 'validation'); } const expectedProvider = configuration.modelProvider?.trim() || 'openai'; if (configValue(args, 'model_provider') !== expectedProvider) { - throw new Error(`Codex command must select configured model provider "${expectedProvider}".`); + throw new ApplicationError(`Codex command must select configured model provider "${expectedProvider}".`, 'validation'); } } @@ -49,10 +50,10 @@ function validateEffortSelection(configuration: AgentConfiguration, args: readon if (!effort) return; if (configuration.provider === 'codex') { if (!hasConfig(args, 'model_reasoning_effort')) { - throw new Error('Codex command must select effort explicitly with --config model_reasoning_effort=... .'); + throw new ApplicationError('Codex command must select effort explicitly with --config model_reasoning_effort=... .', 'validation'); } if (configValue(args, 'model_reasoning_effort') !== effort) { - throw new Error(`Codex command must select configured effort "${effort}".`); + throw new ApplicationError(`Codex command must select configured effort "${effort}".`, 'validation'); } return; } @@ -64,10 +65,10 @@ function validateEffortSelection(configuration: AgentConfiguration, args: readon return; } if (!hasFlag(args, '--variant')) { - throw new Error('OpenCode command must select effort explicitly with --variant ... .'); + throw new ApplicationError('OpenCode command must select effort explicitly with --variant ... .', 'validation'); } if (flagValue(args, ['--variant']) !== effort) { - throw new Error(`OpenCode command must select configured effort "${effort}".`); + throw new ApplicationError(`OpenCode command must select configured effort "${effort}".`, 'validation'); } } diff --git a/src/application/policies/agent_configuration_validation_policy.ts b/src/application/policies/agent_configuration_validation_policy.ts index 00f2c3de..665c361f 100644 --- a/src/application/policies/agent_configuration_validation_policy.ts +++ b/src/application/policies/agent_configuration_validation_policy.ts @@ -1,10 +1,11 @@ import type { AgentProvider } from '../../domain/agent'; +import { ApplicationError } from '../errors/application_error'; export const SUPPORTED_AGENT_PROVIDERS: readonly AgentProvider[] = ['opencode', 'cursor', 'codex']; export function resolveAgentProvider(value: string): AgentProvider { if (SUPPORTED_AGENT_PROVIDERS.includes(value as AgentProvider)) return value as AgentProvider; - throw new Error(`Unsupported agent provider "${value}". Supported providers: ${SUPPORTED_AGENT_PROVIDERS.join(', ')}.`); + throw new ApplicationError(`Unsupported agent provider "${value}". Supported providers: ${SUPPORTED_AGENT_PROVIDERS.join(', ')}.`, 'validation'); } export function resolveModelProvider(value: string | undefined, environment: Record): string { @@ -16,7 +17,7 @@ export function resolveModelProvider(value: string | undefined, environment: Rec export function resolveModel(value: string): string { const model = value.trim(); - if (!model) throw new Error('Agent model must not be empty.'); + if (!model) throw new ApplicationError('Agent model must not be empty.', 'validation'); assertIdentifier(model, 'Agent model must be a simple model identifier without whitespace or shell syntax.', /^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/); return model; } @@ -30,22 +31,22 @@ export function resolveEffort(value: string | undefined): string | undefined { export function assertModelAllowlisted(modelProvider: string, model: string, environment: Record): void { const allowedModels = parseAllowlist(environment.AGENT_ALLOWED_MODELS); if (allowedModels.length > 0 && !allowedModels.includes(`${modelProvider}/${model}`) && !allowedModels.includes(model)) { - throw new Error(`Agent model "${modelProvider}/${model}" is not allowlisted.`); + throw new ApplicationError(`Agent model "${modelProvider}/${model}" is not allowlisted.`, 'authorization'); } } function assertAllowlisted(name: string, value: string, environment: Record): void { const values = parseAllowlist(environment[name]); - if (values.length > 0 && !values.includes(value)) throw new Error(`Agent model provider "${value}" is not allowlisted.`); + if (values.length > 0 && !values.includes(value)) throw new ApplicationError(`Agent model provider "${value}" is not allowlisted.`, 'authorization'); } function parseAllowlist(raw: string | undefined): string[] { if (!raw?.trim()) return []; const values = raw.split(',').map(value => value.trim().toLowerCase()).filter(Boolean); - if (values.length === 0) throw new Error('Agent allowlist must contain at least one value.'); + if (values.length === 0) throw new ApplicationError('Agent allowlist must contain at least one value.', 'configuration'); return values; } function assertIdentifier(value: string, message: string, pattern = /^[a-z0-9][a-z0-9_-]*$/i): void { - if (!pattern.test(value)) throw new Error(message); + if (!pattern.test(value)) throw new ApplicationError(message, 'validation'); } diff --git a/src/application/policies/configuration_persistence_policy.ts b/src/application/policies/configuration_persistence_policy.ts index 430e3934..c73be292 100644 --- a/src/application/policies/configuration_persistence_policy.ts +++ b/src/application/policies/configuration_persistence_policy.ts @@ -1,4 +1,9 @@ -import type { Execution } from '../../data/model/execution'; +export interface ConfigurationPersistenceContext { + readonly isSingleAction: boolean; + readonly singleAction: { + readonly isRecommendStepsAction: boolean; + }; +} /** * Decides whether the completion phase has persistent execution state to save. @@ -9,7 +14,7 @@ import type { Execution } from '../../data/model/execution'; * Recommendation actions are the exception because they persist their * fingerprint and latest recommendation in the hidden issue configuration. */ -export function shouldPersistConfiguration(execution: Pick): boolean { +export function shouldPersistConfiguration(execution: ConfigurationPersistenceContext): boolean { if (!execution.isSingleAction) return true; return execution.singleAction.isRecommendStepsAction; } diff --git a/src/application/policies/deploy_workflow_policy.ts b/src/application/policies/deploy_workflow_policy.ts index 81d781d9..67cc70a0 100644 --- a/src/application/policies/deploy_workflow_policy.ts +++ b/src/application/policies/deploy_workflow_policy.ts @@ -1,6 +1,19 @@ -import type { Execution } from "../../data/model/execution"; import { extractChangelogUpToAdditionalContext } from "../../utils/content_utils"; +export interface DeployWorkflowExecutionContext { + readonly issue: { + readonly labeled: boolean; + readonly labelAdded: string; + readonly number: number; + readonly title: string; + readonly body: string; + }; + readonly labels: { readonly deploy: string }; + readonly release: { readonly active: boolean; readonly branch?: string; readonly version?: string }; + readonly hotfix: { readonly active: boolean; readonly branch?: string; readonly version?: string }; + readonly workflows: { readonly release: string; readonly hotfix: string }; +} + export interface DeployWorkflowPlan { kind: "release" | "hotfix"; branch: string; @@ -11,7 +24,7 @@ export interface DeployWorkflowPlan { issue: number; } -export function resolveDeployWorkflowPlan(param: Execution): DeployWorkflowPlan | undefined { +export function resolveDeployWorkflowPlan(param: DeployWorkflowExecutionContext): DeployWorkflowPlan | undefined { if (!param.issue.labeled || param.issue.labelAdded !== param.labels.deploy) return undefined; if (param.release.active && param.release.branch !== undefined) { diff --git a/src/application/policies/status_command_policy.ts b/src/application/policies/status_command_policy.ts index 862cfe4d..8b1e2d5b 100644 --- a/src/application/policies/status_command_policy.ts +++ b/src/application/policies/status_command_policy.ts @@ -1,5 +1,32 @@ -import type { Execution } from '../../data/model/execution'; import { getResultPayload, Result } from '../../data/model/result'; +import type { CopilotLifecycleLabels } from '../../domain/copilot_lifecycle'; + +export interface CopilotStatusExecutionContext { + readonly owner: string; + readonly repo: string; + readonly eventName: string; + readonly issueNumber: number; + readonly isIssue: boolean; + readonly isPush: boolean; + readonly isPullRequest: boolean; + readonly inputs?: { readonly action?: string }; + readonly issue: { readonly number: number }; + readonly pullRequest: { + readonly number: number; + readonly isPullRequestReviewComment: boolean; + }; + readonly commit: { readonly branch: string }; + readonly labels: { + readonly currentIssueLabels?: readonly string[]; + readonly currentPullRequestLabels?: readonly string[]; + readonly lifecycle?: CopilotLifecycleLabels; + }; + readonly currentConfiguration: { readonly results?: readonly { readonly payload: unknown }[] }; + readonly ai: { + readonly getAiPullRequestDescription: () => boolean; + readonly getPullRequestDescriptionMode?: () => string; + }; +} export interface CopilotStatusSnapshot { readonly owner: string; @@ -19,12 +46,12 @@ export interface CopilotStatusSnapshot { } /** Builds a read-only status snapshot from the facts already loaded by setup. */ -export function buildCopilotStatusSnapshot(execution: Execution): CopilotStatusSnapshot { +export function buildCopilotStatusSnapshot(execution: CopilotStatusExecutionContext): CopilotStatusSnapshot { const issueLabels = [...(execution.labels?.currentIssueLabels ?? [])]; const pullRequestLabels = [...(execution.labels?.currentPullRequestLabels ?? [])]; const isPullRequestTarget = execution.isPullRequest || execution.pullRequest?.number > 0 || execution.pullRequest?.isPullRequestReviewComment; const targetLabels = isPullRequestTarget ? pullRequestLabels : issueLabels; - const lifecycleLabels = execution.labels?.lifecycle ?? {}; + const lifecycleLabels: Partial = execution.labels?.lifecycle ?? {}; const lifecycle = Object.entries({ planned: lifecycleLabels.planned, 'in-progress': lifecycleLabels.inProgress, @@ -67,7 +94,7 @@ export function buildCopilotStatusSnapshot(execution: Execution): CopilotStatusS }; } -export function buildCopilotStatusResult(execution: Execution, taskId: string): Result { +export function buildCopilotStatusResult(execution: CopilotStatusExecutionContext, taskId: string): Result { const snapshot = buildCopilotStatusSnapshot(execution); return new Result({ id: `${taskId}.Status`, diff --git a/src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts b/src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts new file mode 100644 index 00000000..2394d580 --- /dev/null +++ b/src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts @@ -0,0 +1,160 @@ +import { buildGithubActionEventInputs } from '../../../../actions/github_event_inputs'; +import { Issue } from '../../../../data/model/issue'; +import { PullRequest } from '../../../../data/model/pull_request'; +import { Tokens } from '../../../../data/model/tokens'; +import { DEFAULT_COPILOT_LIFECYCLE_LABELS } from '../../../../domain/copilot_lifecycle'; +import { + SynchronizeLifecycleStateUseCase, + type LifecycleSynchronizationExecution, +} from '../synchronize_lifecycle_state_use_case'; + +interface ReplayCase { + name: string; + eventName: string; + action: string; + payload: Record; + initialLabels: string[]; + expectedLabels: string[]; +} + +const REPLAY_CASES: readonly ReplayCase[] = [ + { + name: 'approved review', + eventName: 'pull_request_review', + action: 'submitted', + payload: { review: { state: 'approved', pull_request: { number: 42 } } }, + initialLabels: ['size: M', 'state:reviewing'], + expectedLabels: ['size: M', 'state:ready', 'state:awaiting-maintainer'], + }, + { + name: 'requested changes review', + eventName: 'pull_request_review', + action: 'submitted', + payload: { review: { state: 'changes_requested', pull_request: { number: 42 } } }, + initialLabels: ['state:ready'], + expectedLabels: ['state:changes-requested', 'state:awaiting-issue-author'], + }, + { + name: 'pending check suite', + eventName: 'check_suite', + action: 'requested', + payload: { + check_suite: { + status: 'queued', + conclusion: null, + pull_requests: [{ number: 42 }], + }, + }, + initialLabels: ['state:ready'], + expectedLabels: ['state:reviewing'], + }, + { + name: 'failed workflow run', + eventName: 'workflow_run', + action: 'completed', + payload: { + workflow_run: { + status: 'completed', + conclusion: 'failure', + pull_requests: [{ number: 42 }], + }, + }, + initialLabels: ['state:reviewing'], + expectedLabels: ['state:blocked', 'state:awaiting-maintainer'], + }, + { + name: 'successful workflow run', + eventName: 'workflow_run', + action: 'completed', + payload: { + workflow_run: { + status: 'completed', + conclusion: 'success', + pull_requests: [{ number: 42 }], + }, + }, + initialLabels: ['state:changes-requested'], + expectedLabels: ['state:reviewing'], + }, +]; + +describe('lifecycle event replay integration', () => { + it.each(REPLAY_CASES)('replays $name deterministically', async (replay) => { + const execution = executionFromReplay(replay); + const setLabels = jest.fn(async (_owner: string, _repo: string, _number: number, labels: string[]) => { + execution.labels.currentPullRequestLabels = labels; + }); + const useCase = new SynchronizeLifecycleStateUseCase({ + getLabels: async () => [...execution.labels.currentPullRequestLabels], + setLabels, + }); + + const results = await useCase.invoke({ execution, results: [] }); + + expect(setLabels).toHaveBeenCalledWith('owner', 'repo', 42, replay.expectedLabels, 'token'); + expect(execution.labels.currentPullRequestLabels).toEqual(replay.expectedLabels); + expect(results[0]).toMatchObject({ + id: 'SynchronizeCopilotLifecycleStateUseCase', + success: true, + executed: true, + }); + }); + + it('skips ambiguous check-suite events instead of writing to an arbitrary pull request', async () => { + const inputs = buildGithubActionEventInputs({ + eventName: 'check_suite', + actor: 'octocat', + repo: { owner: 'owner', repo: 'repo' }, + payload: { + action: 'completed', + check_suite: { + status: 'completed', + conclusion: 'failure', + pull_requests: [{ number: 41 }, { number: 42 }], + }, + }, + }); + const execution = executionFromInputs(inputs, ['state:reviewing']); + const setLabels = jest.fn(); + const useCase = new SynchronizeLifecycleStateUseCase({ + getLabels: jest.fn(), + setLabels, + }); + + expect(await useCase.invoke({ execution, results: [] })).toEqual([]); + expect(setLabels).not.toHaveBeenCalled(); + }); +}); + +function executionFromReplay(replay: ReplayCase): LifecycleSynchronizationExecution { + const inputs = buildGithubActionEventInputs({ + eventName: replay.eventName, + actor: 'octocat', + repo: { owner: 'owner', repo: 'repo' }, + payload: { ...replay.payload, action: replay.action }, + }); + return executionFromInputs(inputs, replay.initialLabels); +} + +function executionFromInputs( + inputs: ReturnType, + currentPullRequestLabels: string[], +): LifecycleSynchronizationExecution { + return { + owner: 'owner', + repo: 'repo', + eventName: inputs.eventName, + inputs, + issueNumber: -1, + isIssue: false, + isPullRequest: true, + issue: new Issue(false, false, 0, inputs), + pullRequest: new PullRequest(0, 0, 0, inputs), + labels: { + currentIssueLabels: [], + currentPullRequestLabels, + lifecycle: DEFAULT_COPILOT_LIFECYCLE_LABELS, + }, + tokens: new Tokens('token'), + }; +} diff --git a/src/application/usecases/actions/create_release_policy.ts b/src/application/usecases/actions/create_release_policy.ts index de899771..60f64732 100644 --- a/src/application/usecases/actions/create_release_policy.ts +++ b/src/application/usecases/actions/create_release_policy.ts @@ -1,4 +1,5 @@ import { INPUT_KEYS } from '../../../utils/constants'; +import { ApplicationError } from '../../errors/application_error'; const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/; @@ -25,6 +26,6 @@ export function normalizeVersion(version: string): string | undefined { export function versionForRelease(version: string): string { const normalized = normalizeVersion(version); - if (normalized === undefined) throw new Error('Cannot build a release version from invalid input.'); + if (normalized === undefined) throw new ApplicationError('Cannot build a release version from invalid input.', 'validation'); return `v${normalized}`; } diff --git a/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts b/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts index ae416ea4..e512afc5 100644 --- a/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts +++ b/src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts @@ -1,5 +1,6 @@ -import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; +import type { ExecutionInputs } from '../../../data/model/execution_inputs'; +import type { CopilotLifecycleLabels } from '../../../domain/copilot_lifecycle'; import { lifecycleLabelNames, lifecycleStateLabel, waitingLabelNames, waitingStateLabel } from '../../../domain/copilot_lifecycle'; import { readLifecycleExternalEvidence, resolveLifecycleState } from '../../policies/lifecycle_state_policy'; import { resolveLifecycleWaitingState, type LifecycleWaitingStateDecision } from '../../policies/lifecycle_waiting_state_policy'; @@ -7,10 +8,37 @@ import type { IssueLabelsPort } from '../../ports/issue_management_ports'; import { logDebugInfo, logError } from '../../ports/logging_ports'; export interface SynchronizeLifecycleStateParam { - execution: Execution; + execution: LifecycleSynchronizationExecution; results: readonly Result[]; } +/** Narrow runtime context required by lifecycle reconciliation. */ +export interface LifecycleSynchronizationExecution { + readonly owner: string; + readonly repo: string; + readonly eventName: string; + readonly inputs: ExecutionInputs | undefined; + readonly issueNumber: number; + readonly isIssue: boolean; + readonly isPullRequest: boolean; + readonly issue: { + readonly number: number; + readonly opened: boolean; + readonly descriptionEdited: boolean; + }; + readonly pullRequest: { + readonly number: number; + readonly isMerged: boolean; + readonly isClosed: boolean; + }; + readonly labels: { + currentIssueLabels: string[]; + currentPullRequestLabels: string[]; + readonly lifecycle: CopilotLifecycleLabels; + }; + readonly tokens: { readonly token: string }; +} + const PULL_REQUEST_LIFECYCLE_EVENTS = [ 'pull_request', 'pull_request_review', @@ -92,7 +120,7 @@ export class SynchronizeLifecycleStateUseCase { } } -function targetNumber(execution: Execution): number { +function targetNumber(execution: LifecycleSynchronizationExecution): number { if (['issues', 'issue_comment', 'push'].includes(execution.eventName)) { return execution.issue.number > 0 ? execution.issue.number : execution.issueNumber; } @@ -100,13 +128,13 @@ function targetNumber(execution: Execution): number { return -1; } -function targetLabels(execution: Execution): string[] { +function targetLabels(execution: LifecycleSynchronizationExecution): string[] { return PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName) ? execution.labels.currentPullRequestLabels : execution.labels.currentIssueLabels; } -function setTargetLabels(execution: Execution, labels: string[]): void { +function setTargetLabels(execution: LifecycleSynchronizationExecution, labels: string[]): void { if (PULL_REQUEST_LIFECYCLE_EVENTS.includes(execution.eventName)) { execution.labels.currentPullRequestLabels = labels; } diff --git a/src/application/usecases/execution/resolve_github_execution_admission_use_case.ts b/src/application/usecases/execution/resolve_github_execution_admission_use_case.ts index 2a8975ab..a39ced6b 100644 --- a/src/application/usecases/execution/resolve_github_execution_admission_use_case.ts +++ b/src/application/usecases/execution/resolve_github_execution_admission_use_case.ts @@ -1,4 +1,5 @@ import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports'; +import { ApplicationError } from '../../errors/application_error'; import type { ParamUseCase } from '../base/param_usecase'; import { resolveGithubExecutionAdmission, @@ -25,7 +26,7 @@ export class ResolveGithubExecutionAdmissionUseCase implements ParamUseCase { const tokenUser = await this.authenticatedUserPort.getUserFromToken(request.token); if (typeof tokenUser !== 'string' || tokenUser.trim().length === 0) { - throw new Error('Failed to get user from token'); + throw new ApplicationError('Failed to get user from token', 'authorization'); } return { diff --git a/src/application/usecases/execution/setup_execution_workflow.ts b/src/application/usecases/execution/setup_execution_workflow.ts index c16536e1..30d035a3 100644 --- a/src/application/usecases/execution/setup_execution_workflow.ts +++ b/src/application/usecases/execution/setup_execution_workflow.ts @@ -1,4 +1,5 @@ import type { ExecutionConfigurationPort } from '../../ports/execution_configuration_ports'; +import { ApplicationError } from '../../errors/application_error'; import type { ExecutionIssueSetupPort, ExecutionOrganizationSetupPort } from '../../ports/execution_setup_ports'; import type { Execution } from '../../../data/model/execution'; import { shouldSkipInitialLabelsFetch } from '../../../data/model/initial_labels_policy'; @@ -35,7 +36,7 @@ export async function runSetupExecution(execution: Execution, dependencies: Setu async function loadTokenUser(execution: Execution, organizationSetupPort: ExecutionOrganizationSetupPort): Promise { if (execution.tokenUser !== undefined) return; execution.tokenUser = await organizationSetupPort.getUserFromToken(execution.tokens.token); - if (!execution.tokenUser) throw new Error('Failed to get user from token'); + if (!execution.tokenUser) throw new ApplicationError('Failed to get user from token', 'authorization'); } async function loadPreviousConfiguration(execution: Execution, configurationPort: ExecutionConfigurationPort) { diff --git a/src/application/usecases/setup/setup_credentials_use_case.ts b/src/application/usecases/setup/setup_credentials_use_case.ts index 3747e878..d2bb498b 100644 --- a/src/application/usecases/setup/setup_credentials_use_case.ts +++ b/src/application/usecases/setup/setup_credentials_use_case.ts @@ -10,6 +10,7 @@ import type { SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort, } from '../../ports/setup_wizard_ports'; +import { ApplicationError } from '../../errors/application_error'; export interface SetupCredentialsRequest { owner: string; @@ -38,13 +39,13 @@ export class SetupCredentialsUseCase { async collect(request: SetupCredentialsRequest): Promise { const setupCheck = await this.validation.validateSetupPat(request.owner, request.repository, request.setupToken); if (setupCheck.status !== 'valid') { - throw new Error(`Setup PAT validation failed: ${setupCheck.message}`); + throw new ApplicationError(`Setup PAT validation failed: ${setupCheck.message}`, 'authorization'); } if (!request.manageSecrets) { this.prompt.showCredentialChecks([setupCheck]); return { collection: { apiKeys: [] }, checks: [setupCheck], existingSecretNames: [] }; } - if (!this.secrets) throw new Error('Repository Secret provisioning is not available in this installation.'); + if (!this.secrets) throw new ApplicationError('Repository Secret provisioning is not available in this installation.', 'configuration'); const existingSecretNames = await this.secrets.list(request.owner, request.repository, request.setupToken); const requirements = request.requirements.filter(requirement => requirement.name !== 'SETUP_PAT'); @@ -74,7 +75,7 @@ export class SetupCredentialsUseCase { checks.push(remoteCheck); const decision = await this.prompt.chooseExistingCredential(requirement, remoteCheck); if (remoteCheck.status === 'invalid' && decision !== 'replace') { - throw new Error(`${requirement.name} is invalid and must be replaced before setup can continue.`); + throw new ApplicationError(`${requirement.name} is invalid and must be replaced before setup can continue.`, 'authorization'); } if (decision === 'keep') continue; if (decision === 'skip') continue; @@ -85,14 +86,14 @@ export class SetupCredentialsUseCase { : await this.prompt.requestApiKey(requirement, existing ? checks[checks.length - 1] : undefined); if (!value) { if (!existing) checks.push({ name: requirement.name, status: 'missing', message: 'No value was provided.' }); - throw new Error(`${requirement.name} is required by the selected workflows.`); + throw new ApplicationError(`${requirement.name} is required by the selected workflows.`, 'configuration'); } const check = requirement.kind === 'workflowPat' ? await this.validation.validateSetupPat(request.owner, request.repository, value.value) : await this.validation.validateCredential(requirement, value.value); checks.push({ ...check, name: requirement.name }); if (check.status !== 'valid') { - throw new Error(`${requirement.name} validation failed: ${check.message}`); + throw new ApplicationError(`${requirement.name} validation failed: ${check.message}`, 'authorization'); } values.push(value); } diff --git a/src/application/usecases/setup/setup_wizard_use_case.ts b/src/application/usecases/setup/setup_wizard_use_case.ts index a0df36d0..8d66d0e9 100644 --- a/src/application/usecases/setup/setup_wizard_use_case.ts +++ b/src/application/usecases/setup/setup_wizard_use_case.ts @@ -1,4 +1,5 @@ import type { SetupPromptPort } from '../../ports/setup_wizard_ports'; +import { ApplicationError } from '../../errors/application_error'; import type { SetupConfiguration, SetupPlan } from '../../../domain/setup'; import { buildSetupPlan, @@ -30,7 +31,10 @@ export class SetupWizardUseCase { : collected; const validationErrors = validateSetupConfiguration(configuration); if (validationErrors.length > 0) { - throw new Error(`Invalid setup configuration:\n${validationErrors.map(error => `- ${error}`).join('\n')}`); + throw new ApplicationError( + `Invalid setup configuration:\n${validationErrors.map(error => `- ${error}`).join('\n')}`, + 'validation', + ); } const plan = buildSetupPlan(configuration); this.prompt.showPlan(plan); diff --git a/src/application/usecases/steps/commit/bugbot/bugbot_autofix_postflight.ts b/src/application/usecases/steps/commit/bugbot/bugbot_autofix_postflight.ts index fb00ec80..9a657b07 100644 --- a/src/application/usecases/steps/commit/bugbot/bugbot_autofix_postflight.ts +++ b/src/application/usecases/steps/commit/bugbot/bugbot_autofix_postflight.ts @@ -1,5 +1,6 @@ import type { Execution } from '../../../../../data/model/execution'; import { Result } from '../../../../../data/model/result'; +import { ApplicationError } from '../../../../errors/application_error'; import type { GitCommitPort } from '../../../../../application/ports/git_ports'; import type { BugbotContext } from './types'; import { isSensitiveWorkspacePath, listWorkspacePaths, selectWorkspacePathsToCommit } from './workspace_changes'; @@ -42,7 +43,11 @@ async function inspectWorkspace(gitCommitPort: GitCommitPort, phase: string): Pr try { return await listWorkspacePaths(gitCommitPort); } catch (error) { - throw new Error(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`); + throw new ApplicationError( + `Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`, + 'provider', + { cause: error, retryable: true }, + ); } } diff --git a/src/application/usecases/steps/commit/bugbot/bugbot_autofix_preflight.ts b/src/application/usecases/steps/commit/bugbot/bugbot_autofix_preflight.ts index fbfa2dad..f8401530 100644 --- a/src/application/usecases/steps/commit/bugbot/bugbot_autofix_preflight.ts +++ b/src/application/usecases/steps/commit/bugbot/bugbot_autofix_preflight.ts @@ -1,5 +1,6 @@ import type { Execution } from '../../../../../data/model/execution'; import { Result } from '../../../../../data/model/result'; +import { ApplicationError } from '../../../../errors/application_error'; import type { GitCommitPort } from '../../../../../application/ports/git_ports'; import type { BugbotContextPorts } from '../../../../../application/ports/bugbot_context_ports'; import type { BugbotContext } from './types'; @@ -53,7 +54,11 @@ async function inspectWorkspace(gitCommitPort: GitCommitPort, phase: string): Pr try { return await listWorkspacePaths(gitCommitPort); } catch (error) { - throw new Error(`Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`); + throw new ApplicationError( + `Unable to inspect workspace ${phase} autofix: ${error instanceof Error ? error.message : String(error)}`, + 'provider', + { cause: error, retryable: true }, + ); } } diff --git a/src/application/usecases/steps/commit/bugbot/marker.ts b/src/application/usecases/steps/commit/bugbot/marker.ts index d286b01d..8d21bb22 100644 --- a/src/application/usecases/steps/commit/bugbot/marker.ts +++ b/src/application/usecases/steps/commit/bugbot/marker.ts @@ -6,6 +6,7 @@ */ import { BUGBOT_MARKER_PREFIX } from "../../../../../utils/constants"; +import { ApplicationError } from "../../../../errors/application_error"; import type { BugbotFinding, BugbotFindingResolution } from "./types"; import { sanitizeAgentMarkdown } from "../../../../../application/policies/github_comment_publication_policy"; @@ -37,12 +38,13 @@ export function normalizeFindingIdForMarker( function requireFindingIdForMarker(findingId: string): string { const safeId = normalizeFindingIdForMarker(findingId); if (safeId == null) { - throw new Error( + throw new ApplicationError( findingId.trim().length === 0 ? "Finding ID is empty after marker sanitization." : findingId.trim().length > MAX_FINDING_ID_LENGTH ? "Finding ID exceeds the maximum marker length." : "Finding ID contains marker-breaking characters.", + 'validation', ); } return safeId; diff --git a/src/application/usecases/steps/common/store_configuration_use_case.ts b/src/application/usecases/steps/common/store_configuration_use_case.ts index c82b0093..92e744e0 100644 --- a/src/application/usecases/steps/common/store_configuration_use_case.ts +++ b/src/application/usecases/steps/common/store_configuration_use_case.ts @@ -1,4 +1,5 @@ import { Execution } from "../../../../data/model/execution"; +import { ApplicationError } from '../../../errors/application_error'; import type { ConfigurationStorePort } from "../../../ports/configuration_store_ports"; import { logError, logInfo } from "../../../ports/logging_ports"; import { getTaskEmoji } from "../../../../utils/task_emoji"; @@ -20,7 +21,7 @@ export class StoreConfigurationUseCase implements ParamUseCase ) } catch (error) { logError(`StoreConfiguration: failed to update configuration.`, error instanceof Error ? { stack: (error as Error).stack } : undefined); - throw new Error('Configuration persistence failed.'); + throw new ApplicationError('Configuration persistence failed.', 'provider', { cause: error, retryable: true }); } } } diff --git a/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts b/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts index 3008fe0d..0df0be31 100644 --- a/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts +++ b/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts @@ -12,6 +12,7 @@ import { type WorkflowPollingPolicy, } from '../../policies/workflow_queue_policy'; import type { ParamUseCase } from '../base/param_usecase'; +import { ApplicationError } from '../../errors/application_error'; const SYSTEM_CLOCK: WorkflowQueueClockPort = { nowMilliseconds: () => Date.now() }; const SYSTEM_RANDOM: WorkflowPollingRandomPort = { next: () => Math.random() }; @@ -34,13 +35,13 @@ export class WaitForPreviousWorkflowRunsUseCase implements ParamUseCase= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } const activeRunCount = await this.queryPort.countActivePreviousRuns(query, { deadlineAtMilliseconds, }); if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } if (activeRunCount === 0) { this.observerPort.noActivePreviousRuns(); @@ -53,7 +54,7 @@ export class WaitForPreviousWorkflowRunsUseCase implements ParamUseCase= deadlineAtMilliseconds) { - throw new Error('Timeout waiting for previous runs to finish.'); + throw queueTimeoutError(); } this.observerPort.waitingForPreviousRuns(activeRunCount, delayMilliseconds); await this.delayPort.wait(delayMilliseconds); @@ -61,3 +62,7 @@ export class WaitForPreviousWorkflowRunsUseCase implements ParamUseCase { it('ignores malformed external data without throwing', () => { @@ -67,4 +67,46 @@ describe('Config', () => { expect(c.recommendationState).toBeUndefined(); }); + + it('migrates legacy payloads and removes transient execution results', () => { + const migration = migrateConfigurationPayload({ + branchType: 'feature', + results: [{ id: 'runtime-only' }], + }); + + expect(migration).toMatchObject({ + sourceVersion: 0, + migrated: true, + futureVersion: false, + }); + expect(migration.payload).toEqual({ + branchType: 'feature', + schemaVersion: CONFIG_SCHEMA_VERSION, + }); + expect(new Config({ branchType: 'feature', results: [] }).schemaVersion).toBe(CONFIG_SCHEMA_VERSION); + }); + + it('drops malformed durable values during migration', () => { + const migration = migrateConfigurationPayload({ + schemaVersion: 1, + branchConfiguration: null, + recommendationState: { recommendation: 'incomplete' }, + }); + + expect(migration.payload).toEqual({ schemaVersion: CONFIG_SCHEMA_VERSION }); + }); + + it('does not downgrade a payload produced by a newer version', () => { + const migration = migrateConfigurationPayload({ + schemaVersion: CONFIG_SCHEMA_VERSION + 10, + futureField: 'preserve-me', + }); + + expect(migration).toMatchObject({ + sourceVersion: CONFIG_SCHEMA_VERSION + 10, + migrated: false, + futureVersion: true, + }); + expect(new Config(migration.payload).schemaVersion).toBe(CONFIG_SCHEMA_VERSION + 10); + }); }); diff --git a/src/data/model/config.ts b/src/data/model/config.ts index 91d36738..04765181 100644 --- a/src/data/model/config.ts +++ b/src/data/model/config.ts @@ -3,7 +3,53 @@ import {isRecommendationState, RecommendationState} from "./recommendation_state import {Result} from "./result"; import { asModelInput, readOptionalString, readString } from './model_input'; -export const CONFIG_SCHEMA_VERSION = 1; +/** Version of the durable configuration contract stored in issue/PR content. */ +export const CONFIG_SCHEMA_VERSION = 2; + +export interface ConfigurationMigrationResult { + readonly payload: Record; + readonly sourceVersion: number; + readonly migrated: boolean; + readonly futureVersion: boolean; +} + +/** + * Normalizes persisted configuration without silently losing fields from a + * newer installation. Unknown keys are deliberately retained so a downgrade + * or a mixed-version workflow can round-trip data safely. + */ +export function migrateConfigurationPayload(value: unknown): ConfigurationMigrationResult { + const original = { ...asModelInput(value) }; + const sourceVersion = readSchemaVersion(original['schemaVersion']); + + if (sourceVersion > CONFIG_SCHEMA_VERSION) { + return { + payload: original, + sourceVersion, + migrated: false, + futureVersion: true, + }; + } + + const payload = { ...original }; + const hadTransientResults = Object.prototype.hasOwnProperty.call(payload, 'results'); + delete payload.results; + + if (payload.branchConfiguration === null) delete payload.branchConfiguration; + if (!isRecommendationState(payload.recommendationState)) delete payload.recommendationState; + payload.schemaVersion = CONFIG_SCHEMA_VERSION; + + return { + payload, + sourceVersion, + migrated: sourceVersion !== CONFIG_SCHEMA_VERSION || hadTransientResults, + futureVersion: false, + }; +} + +function readSchemaVersion(value: unknown): number { + return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : 0; +} export class Config { readonly schemaVersion: number; @@ -18,12 +64,8 @@ export class Config { recommendationState: RecommendationState | undefined; constructor(data: unknown) { - const input = asModelInput(data); - this.schemaVersion = typeof input.schemaVersion === 'number' - && Number.isInteger(input.schemaVersion) - && input.schemaVersion > 0 - ? input.schemaVersion - : CONFIG_SCHEMA_VERSION; + const input = asModelInput(migrateConfigurationPayload(data).payload); + this.schemaVersion = readSchemaVersion(input.schemaVersion) || CONFIG_SCHEMA_VERSION; this.branchType = readString(input, 'branchType'); this.hotfixOriginBranch = readOptionalString(input, 'hotfixOriginBranch'); this.hotfixBranch = readOptionalString(input, 'hotfixBranch'); diff --git a/src/manager/description/__tests__/configuration_handler.test.ts b/src/manager/description/__tests__/configuration_handler.test.ts index 9bf3c311..f1fce60d 100644 --- a/src/manager/description/__tests__/configuration_handler.test.ts +++ b/src/manager/description/__tests__/configuration_handler.test.ts @@ -129,6 +129,30 @@ describe('ConfigurationHandler', () => { expect(parsed.branchConfiguration).toEqual({ name: 'leaf' }); }); + it('does not downgrade a configuration written by a newer workflow version', async () => { + const storedJson = JSON.stringify({ + schemaVersion: 99, + parentBranch: 'main', + futurePolicy: { preserve: true }, + }); + mockGetDescription.mockResolvedValue(descriptionWithConfig(storedJson)); + mockUpdateDescription.mockResolvedValue(undefined); + + await handler.update(minimalExecution({ + currentConfiguration: { + branchType: 'feature', + parentBranch: undefined, + branchConfiguration: undefined, + }, + })); + + const fullDesc = mockUpdateDescription.mock.calls[0][3]; + const parsed = JSON.parse(handler.getContent(fullDesc)!.trim()); + expect(parsed.schemaVersion).toBe(99); + expect(parsed.futurePolicy).toEqual({ preserve: true }); + expect(parsed.parentBranch).toBe('main'); + }); + it('preserves workingBranch from stored when current workingBranch is undefined (PR edited event)', async () => { const storedJson = JSON.stringify({ branchType: 'bugfix', diff --git a/src/manager/description/configuration_payload_policy.ts b/src/manager/description/configuration_payload_policy.ts index 34264ed6..8b97a197 100644 --- a/src/manager/description/configuration_payload_policy.ts +++ b/src/manager/description/configuration_payload_policy.ts @@ -1,8 +1,21 @@ -import type { Execution } from '../../data/model/execution'; -import { CONFIG_SCHEMA_VERSION } from '../../data/model/config'; +import { CONFIG_SCHEMA_VERSION, migrateConfigurationPayload } from '../../data/model/config'; -export function buildConfigurationPayload(execution: Execution, storedRaw: string | undefined): string { +export interface ConfigurationPayloadContext { + readonly currentConfiguration: { + readonly branchType: string; + readonly releaseBranch?: string; + readonly workingBranch?: string; + readonly parentBranch?: string; + readonly hotfixOriginBranch?: string; + readonly hotfixBranch?: string; + readonly branchConfiguration?: unknown; + readonly recommendationState?: unknown; + }; +} + +export function buildConfigurationPayload(execution: ConfigurationPayloadContext, storedRaw: string | undefined): string { const current = execution.currentConfiguration; + const stored = parseStoredConfiguration(storedRaw); const payload: Record = { schemaVersion: CONFIG_SCHEMA_VERSION, branchType: current.branchType, @@ -14,7 +27,8 @@ export function buildConfigurationPayload(execution: Execution, storedRaw: strin branchConfiguration: current.branchConfiguration, recommendationState: current.recommendationState, }; - mergeMissingValues(payload, parseStoredConfiguration(storedRaw)); + mergeMissingValues(payload, stored); + preserveFutureSchemaVersion(payload, stored); delete payload.results; return JSON.stringify(payload, null, 4); } @@ -22,7 +36,7 @@ export function buildConfigurationPayload(execution: Execution, storedRaw: strin function parseStoredConfiguration(storedRaw: string | undefined): Record | undefined { if (!storedRaw?.trim()) return undefined; try { - return JSON.parse(storedRaw) as Record; + return migrateConfigurationPayload(JSON.parse(storedRaw)).payload; } catch { return undefined; } @@ -34,3 +48,12 @@ function mergeMissingValues(payload: Record, stored: Record, + stored: Record | undefined, +): void { + if (typeof stored?.schemaVersion === 'number' && stored.schemaVersion > CONFIG_SCHEMA_VERSION) { + payload.schemaVersion = stored.schemaVersion; + } +} diff --git a/src/tooling/__tests__/collect_architecture_metrics.test.ts b/src/tooling/__tests__/collect_architecture_metrics.test.ts index 14948bd1..d96ce27f 100644 --- a/src/tooling/__tests__/collect_architecture_metrics.test.ts +++ b/src/tooling/__tests__/collect_architecture_metrics.test.ts @@ -24,6 +24,9 @@ describe("collect architecture metrics", () => { expect(commandTimeout(["pnpm", "exec", "jest", "--coverage"])).toBe( 600_000, ); + expect(commandTimeout(["/repo/node_modules/.bin/jest", "--coverage"])).toBe( + 600_000, + ); expect(commandTimeout(["/opt/repowise", "init", "."])).toBe(900_000); expect(commandTimeout(["/opt/repowise", "health", "."])).toBe(300_000); expect(commandTimeout(["/opt/graphify", "update", "."])).toBe(600_000); From f12576adf34e6ba5adf9df6a2d450761ddf99660 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 4 Sep 2026 11:19:21 +0200 Subject: [PATCH 06/11] develop: add comment-driven copilot assistance --- .cursor/rules/architecture.mdc | 2 +- .cursor/rules/bugbot.mdc | 13 +- .cursor/rules/usecase-flows.mdc | 2 +- .github/ISSUE_TEMPLATE/help_request.yml | 5 + README.md | 4 +- docs.json | 5 + docs/authentication.mdx | 2 + docs/bugbot/autofix.mdx | 6 +- docs/bugbot/detection.mdx | 11 +- docs/bugbot/do-user-request.mdx | 17 +- docs/bugbot/examples.mdx | 4 +- docs/bugbot/how-it-works.mdx | 10 +- docs/bugbot/permissions.mdx | 4 +- docs/features.mdx | 4 +- docs/issues/comment-commands.mdx | 74 +++++++ docs/issues/index.mdx | 5 +- setup/ISSUE_TEMPLATE/help_request.yml | 5 + .../copilot_interaction_policy.test.ts | 38 ++++ .../policies/copilot_interaction_policy.ts | 70 +++++++ .../ports/actor_authorization_ports.ts | 2 +- ...comment_automation_action_workflow.test.ts | 37 ++++ .../comment_automation_route_policy.test.ts | 18 +- .../comment_automation_use_case.test.ts | 193 +++++++++++++++++- .../__tests__/issue_comment_use_case.test.ts | 1 + .../usecases/__tests__/issue_use_case.test.ts | 33 +++ .../recommend_steps_use_case.test.ts | 17 ++ .../actions/recommend_steps_result_policy.ts | 10 +- .../comment_automation_action_workflow.ts | 125 +++++++----- .../comment_automation_command_workflow.ts | 20 +- .../comment_automation_decision_workflow.ts | 8 + .../comment_automation_route_policy.ts | 6 +- src/application/usecases/issue_workflow.ts | 22 +- .../detect_bugbot_fix_intent_policy.test.ts | 5 +- .../detect_bugbot_fix_intent_use_case.test.ts | 43 +++- .../bugbot/detect_bugbot_fix_intent_policy.ts | 5 +- .../detect_bugbot_fix_intent_workflow.ts | 39 +++- .../usecases/steps/commit/bugbot/schema.ts | 11 +- .../__tests__/think_input_policy.test.ts | 16 +- .../steps/common/think_input_policy.ts | 8 + .../steps/common/think_request_policy.ts | 4 +- .../answer_issue_help_use_case.test.ts | 17 ++ .../steps/issue/answer_issue_help_workflow.ts | 18 +- .../actor_modification_policy.test.ts | 10 +- .../repository/actor_modification_policy.ts | 13 +- .../actor_authorization_repository.test.ts | 53 ++++- .../actor_authorization_repository.ts | 45 +++- src/domain/__tests__/copilot_command.test.ts | 8 +- src/domain/copilot_command.ts | 9 +- .../ports/github_identity_provider_ports.ts | 7 + .../__tests__/bugbot_fix_intent.test.ts | 2 + src/prompts/bugbot_fix_intent.ts | 7 +- 51 files changed, 962 insertions(+), 131 deletions(-) create mode 100644 docs/issues/comment-commands.mdx create mode 100644 src/application/policies/__tests__/copilot_interaction_policy.test.ts create mode 100644 src/application/policies/copilot_interaction_policy.ts create mode 100644 src/application/usecases/__tests__/comment_automation_action_workflow.test.ts diff --git a/.cursor/rules/architecture.mdc b/.cursor/rules/architecture.mdc index 79d3c33a..656c8489 100644 --- a/.cursor/rules/architecture.mdc +++ b/.cursor/rules/architecture.mdc @@ -29,7 +29,7 @@ alwaysApply: true | Steps (commit) | `src/usecase/steps/commit/` | notify commit, check size | | Steps (issue comment) | `src/usecase/steps/issue_comment/` | check_issue_comment_language (translation) | | Steps (PR review comment) | `src/usecase/steps/pull_request_review_comment/` | check_pull_request_comment_language (translation) | -| Bugbot autofix & user request | `src/usecase/steps/commit/bugbot/` + `user_request_use_case.ts` | detect_bugbot_fix_intent_use_case (plan agent: is_fix_request, is_do_request, target_finding_ids), BugbotAutofixUseCase + runBugbotAutofixCommitAndPush (fix findings), DoUserRequestUseCase + runUserRequestCommitAndPush (generic “do this”). Permission: ProjectRepository.isActorAllowedToModifyFiles (org member or repo owner). | +| Bugbot autofix & user request | `src/usecase/steps/commit/bugbot/` + `user_request_use_case.ts` | detect_bugbot_fix_intent_use_case (plan agent: is_fix_request, is_do_request, is_review_request, target_finding_ids), BugbotAutofixUseCase + runBugbotAutofixCommitAndPush (fix findings), DoUserRequestUseCase + runUserRequestCommitAndPush (generic “do this”). Permission: ProjectRepository.isActorAllowedToModifyFiles (org member, or repo owner/write collaborator for personal repos); natural-language mutation requires the bot mention. | | Manager (content) | `src/manager/` | description handlers, configuration_handler, markdown_content_hotfix_handler (PR description, hotfix changelog content) | | Models | `src/data/model/` | Execution, Issue, PullRequest, SingleAction, etc. | | Repos | `src/data/repository/` | branch_repository, issue_repository, workflow_repository, ai_repository (OpenCode), file_repository, project_repository | diff --git a/.cursor/rules/bugbot.mdc b/.cursor/rules/bugbot.mdc index 31e71d75..5e5dca3e 100644 --- a/.cursor/rules/bugbot.mdc +++ b/.cursor/rules/bugbot.mdc @@ -65,13 +65,13 @@ Bugbot has two main modes: **detection** (on push or single action) and **fix/do - `loadBugbotContext(param, { branchOverride })` → unresolved findings. - Build `UnresolvedFindingSummary[]` (id, title from `extractTitleFromBody`, description = fullBody.slice(0, 4000)). - If PR review comment and `commentInReplyToId`: fetch parent comment body (`getPullRequestReviewCommentBody`), slice(0,1500).trim for prompt. - - `buildBugbotFixIntentPrompt(commentBody, unresolvedFindings, parentCommentBody?)` → prompt asks: is_fix_request?, target_finding_ids?, is_do_request? - - `askAgent(OPENCODE_AGENT_PLAN, prompt, BUGBOT_FIX_INTENT_RESPONSE_SCHEMA)` → `{ is_fix_request, target_finding_ids, is_do_request }`. - - Payload: `isFixRequest`, `isDoRequest`, `targetFindingIds` (filtered to valid unresolved ids), `context`, `branchOverride`. + - `buildBugbotFixIntentPrompt(commentBody, unresolvedFindings, parentCommentBody?)` → prompt asks: is_fix_request?, target_finding_ids?, is_do_request?, is_review_request?. + - `askAgent(OPENCODE_AGENT_PLAN, prompt, BUGBOT_FIX_INTENT_RESPONSE_SCHEMA)` → `{ is_fix_request, target_finding_ids, is_do_request, is_review_request }`. + - Payload: `isFixRequest`, `isDoRequest`, `isReviewRequest`, `targetFindingIds` (filtered to valid unresolved ids), `context`, `branchOverride`. 2. **Permission:** `ProjectRepository.isActorAllowedToModifyFiles(owner, actor, token)`. - If owner is Organization: `orgs.checkMembershipForUser` (204 = allowed). - - If owner is User: allowed only if `actor === owner`. + - If owner is User: allowed for the owner or a repository collaborator with `push`, `maintain`, or `admin` permission. 3. **Branch A – Bugbot autofix** (when `canRunBugbotAutofix(payload)` and `allowedToModifyFiles`): - `BugbotAutofixUseCase.invoke({ execution, targetFindingIds, userComment, context, branchOverride })` @@ -84,12 +84,13 @@ Bugbot has two main modes: **detection** (on push or single action) and **fix/do - `buildUserRequestPrompt(execution, userComment)` – repo context + sanitized user request; `copilotMessage(ai, prompt)`. - If success: `runUserRequestCommitAndPush(execution, { branchOverride })` – same verify/checkout/add/commit/push with message `chore(#N): apply user request` or `chore: apply user request`. -5. **Think** (when no file-modifying action ran): `ThinkUseCase.invoke(param)` – answers the user (e.g. question). +5. **Review** (when the bot is mentioned and the request is read-only analysis): the existing findings/review flow runs without file changes. +6. **Think** (when no file-modifying action ran): `ThinkUseCase.invoke(param)` – answers the user (e.g. question). **Key paths (fix/do):** - `detect_bugbot_fix_intent_use_case.ts` – intent detection, branch resolution for issue_comment -- `build_bugbot_fix_intent_prompt.ts` – prompt for is_fix_request / is_do_request / target_finding_ids +- `build_bugbot_fix_intent_prompt.ts` – prompt for is_fix_request / is_do_request / is_review_request / target_finding_ids - `bugbot_fix_intent_payload.ts` – getBugbotFixIntentPayload, canRunBugbotAutofix, canRunDoUserRequest - `schema.ts` – BUGBOT_FIX_INTENT_RESPONSE_SCHEMA (is_fix_request, target_finding_ids, is_do_request) - `bugbot_autofix_use_case.ts` – build prompt, copilotMessage (build agent) diff --git a/.cursor/rules/usecase-flows.mdc b/.cursor/rules/usecase-flows.mdc index ae1ce67e..bcee5460 100644 --- a/.cursor/rules/usecase-flows.mdc +++ b/.cursor/rules/usecase-flows.mdc @@ -143,6 +143,6 @@ Invoked when: ## 8. Flow dependencies -- **Bugbot autofix / Do user request**: require OpenCode, `isActorAllowedToModifyFiles` (org member or repo owner), and on issue_comment optionally branch from PR (`getHeadBranchForIssue`). +- **Bugbot autofix / Do user request**: require OpenCode for natural-language intent, an explicit `@vypbot` mention for natural-language file changes, `isActorAllowedToModifyFiles` (org member, or repo owner / write collaborator for personal repos), and on issue_comment a branch from an open PR (`getHeadBranchForIssue`). Explicit `/copilot fix` and `/copilot implement` commands are deterministic and auditable. - **Think**: used in IssueComment and PullRequestReviewComment when neither autofix nor do user request runs (by intent or by permission). - **CommitUseCase**: NotifyNewCommitOnIssue, CheckChangesIssueSize, CheckProgress, DetectPotentialProblems (bugbot) always run in that order on every push with commits. diff --git a/.github/ISSUE_TEMPLATE/help_request.yml b/.github/ISSUE_TEMPLATE/help_request.yml index 9f9a3dd0..190f2444 100644 --- a/.github/ISSUE_TEMPLATE/help_request.yml +++ b/.github/ISSUE_TEMPLATE/help_request.yml @@ -17,6 +17,11 @@ body: value: | --- + - type: markdown + attributes: + value: | + **Copilot assistance:** After opening this issue, mention `@vypbot` in a comment to ask for an explanation, diagnosis, or read-only code/security analysis. Use `/copilot help` to see all available commands. + - type: dropdown id: help_area attributes: diff --git a/README.md b/README.md index a70d61a3..0e3de789 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ Full documentation: **[docs.page/vypdev/copilot](https://docs.page/vypdev/copilo ## What it does -- **Issues** — Branch creation from labels (feature, bugfix, hotfix, release, docs, chore), project linking, assignees, lifecycle/size/progress labels; optional Bugbot (AI) on the issue; from a comment you can plan, recheck, fix, or dismiss findings. -- **Pull requests** — Link PRs to issues, update project columns, assign reviewers; optional AI-generated PR description and automatic Bugbot review with stable finding threads; from a PR review comment you can request a read-only recheck or an authorized autofix. +- **Issues** — Branch creation from labels (feature, bugfix, hotfix, release, docs, chore), project linking, assignees, lifecycle/size/progress labels; new issues receive a contextual `@vypbot` welcome; from comments you can ask for help, explain/diagnose/analyze code, plan work, fix findings, or request an authorized implementation. +- **Pull requests** — Link PRs to issues, update project columns, assign reviewers; optional AI-generated PR description and automatic Bugbot review with stable finding threads; use `/copilot analyze` or `@vypbot analyze ...` for read-only review, or request an authorized change. - **Push (commits)** — Notify the issue, update size/progress; optional Bugbot (detection) and prefix checks. - **Projects** — Link issues and PRs to boards and move them to the right columns. - **Single actions** — On-demand: check progress, think, create release/tag, mark deployed, etc. diff --git a/docs.json b/docs.json index 6661fafc..7ae37fc3 100644 --- a/docs.json +++ b/docs.json @@ -342,6 +342,11 @@ "title": "Examples", "href": "/issues/examples", "icon": "file-code" + }, + { + "title": "Comment commands", + "href": "/issues/comment-commands", + "icon": "comment" } ] }, diff --git a/docs/authentication.mdx b/docs/authentication.mdx index 9f453b83..cd4a794e 100644 --- a/docs/authentication.mdx +++ b/docs/authentication.mdx @@ -14,6 +14,8 @@ The setup PAT and workflow PAT may have different owners and permissions. Do not **When the event actor is the same as the token user**: The action detects this before entering the workflow queue. It completes successfully without waiting or running the normal issue/PR/push pipeline. A valid explicit single action still runs. This avoids the bot reacting to its own actions. Use a dedicated bot account (different from the actor) if you want full pipeline behavior on every event. +For comment-driven assistance, read-only commands are available to anyone who can comment. File-modifying commands are restricted to organization members in organization repositories. In personal repositories, the repository owner or a collaborator with `push`, `maintain`, or `admin` permission may request changes. The workflow PAT still needs the relevant `contents: write` permission, and issue comments need an open PR to provide a branch for the change. + Choose which account will be used to create your PAT. This account will act as your bot. diff --git a/docs/bugbot/autofix.mdx b/docs/bugbot/autofix.mdx index 9954689a..e12b353f 100644 --- a/docs/bugbot/autofix.mdx +++ b/docs/bugbot/autofix.mdx @@ -35,6 +35,10 @@ Add a **comment** on the issue that references the findings you want fixed. The You don’t have to use exact wording; the configured analysis role interprets intent. Be clear when you want only **some** findings (e.g. “fix the first two” or “fix the one about the login handler”). +For a natural-language comment, mention the configured bot user (for example, +`@vypbot fix the authentication finding`). Explicit `/copilot fix ` +and `/copilot fix all` commands do not require a mention. + **Important:** On **issue comments**, the action needs an **open pull request** that references the issue so it can determine which **branch** to checkout and push to. If there is no such PR, autofix is skipped (the action cannot push without a branch). See [Troubleshooting → Bugbot autofix](/security-operations/operations/troubleshooting#bugbot-autofix). ### From the pull request @@ -58,7 +62,7 @@ want an authorized file-changing operation. Only **certain users** can trigger file-modifying actions (autofix and [do user request](/bugbot/do-user-request)): - **Organization repositories:** The comment author must be a **member of the organization** (checked via GitHub’s `orgs.checkMembershipForUser`). If the author is not a member, the action does **not** run autofix; it can still run **Think** and reply with an answer. -- **User (personal) repositories:** Only the **repository owner** can trigger autofix. Other users get a Think response only. +- **User (personal) repositories:** The **repository owner** or a collaborator with `push`, `maintain`, or `admin` permission can trigger autofix. Other users get a Think response only. This avoids random contributors or external users pushing commits via comments. There is no separate “Bugbot role”; the same rule applies to both autofix and do-user-request. diff --git a/docs/bugbot/detection.mdx b/docs/bugbot/detection.mdx index 7b46aae1..25c31501 100644 --- a/docs/bugbot/detection.mdx +++ b/docs/bugbot/detection.mdx @@ -32,10 +32,13 @@ If the branch is not linked to an issue (e.g. `issueNumber === -1`), detection i ### 3. On demand (comment, single action, or CLI) -From an issue or pull-request comment, `/copilot review`, `/copilot findings`, -or `/copilot recheck` starts the read-only detection flow immediately. On an -issue comment, Copilot resolves the branch from the open PR linked to the -issue. These commands do not invoke autofix and do not modify files. +From an issue or pull-request comment, `/copilot analyze`, `/copilot review`, +`/copilot findings`, or `/copilot recheck` starts the read-only detection flow +immediately. You can also write a natural-language request such as +`@vypbot review this PR for authentication vulnerabilities`; the bot mention +is matched case-insensitively and the request is routed to the same review +flow. On an issue comment, Copilot resolves the branch from the open PR linked +to the issue. These commands do not invoke autofix and do not modify files. You can run Bugbot detection **without pushing**: diff --git a/docs/bugbot/do-user-request.mdx b/docs/bugbot/do-user-request.mdx index bc350577..9d90fdc6 100644 --- a/docs/bugbot/do-user-request.mdx +++ b/docs/bugbot/do-user-request.mdx @@ -5,7 +5,7 @@ description: Ask the bot to apply general code changes (tests, refactors, featur # Do user request -Besides fixing **specific Bugbot findings**, you can ask the bot to perform **general code changes** in the repository: add tests, refactor a function, implement a small feature, update docs, etc. This is called **do user request**. The same permission and workflow setup as [Autofix](/bugbot/autofix) apply: only org members or the repo owner can trigger it, and the workflow must grant **`contents: write`**. +Besides fixing **specific Bugbot findings**, you can ask the bot to perform **general code changes** in the repository: add tests, refactor a function, implement a small feature, update docs, etc. This is called **do user request**. Use `/copilot implement ` for an explicit command, or mention `@vypbot` and describe the request naturally. The same permission and workflow setup as [Autofix](/bugbot/autofix) apply: organization members can trigger it in organization repositories; in personal repositories the repository owner or a collaborator with `push`, `maintain`, or `admin` permission can trigger it. The workflow must grant **`contents: write`**. This page explains how to use it and how it differs from autofix. @@ -17,6 +17,7 @@ When you comment on an **issue** or **pull request**, the action first runs **in - A **fix request** — “fix it”, “fix all”, etc. → [Autofix](/bugbot/autofix) runs (fix specific findings). - A **do request** — “add a test for X”, “refactor this”, “implement feature Y”, etc. → **Do user request** runs (general code change). +- **Read-only review** — “analyze this PR for vulnerabilities” → the review flow runs (no file changes). - **Neither** — e.g. a question → **Think** runs (answer only, no file changes). So you don’t choose a “mode”; you just write what you want. If the agent classifies it as a do request and you have permission, the action runs the configured **execution role** with your request, then runs the same **verify commands** as for autofix and commits and pushes. @@ -36,13 +37,21 @@ Write a **comment** on the issue or on the PR (or, for PRs, you can reply in a r - “add error handling for the API call” - “fix the typo in the docstring” +Explicit form: + +```text +/copilot implement add regression tests for the null response case +``` + +The `implement` command is useful when you want an auditable, unambiguous request. General requests are now classified even when the issue has no previously reported Bugbot findings. + You can be brief or detailed. The configured execution role will apply the changes in its workspace; the action then runs **verify commands** (from `bugbot-fix-verify-commands`) and, if they pass, commits and pushes. --- ## Permissions and workflow -- **Who can trigger:** Same as [Autofix](/bugbot/autofix): **organization members** (for org repos) or the **repository owner** (for user repos). Others get a Think response only. +- **Who can trigger:** Organization members for organization repositories. For personal repositories, the repository owner or a collaborator with `push`, `maintain`, or `admin` permission. Others get a Think response only. - **Workflow:** The workflow that runs on `issue_comment` or `pull_request_review_comment` must grant **`contents: write`** so the action can push. - **Branch:** On **issue comment**, the action resolves the branch from an open PR that references the issue (same as autofix). On **PR comment** or **PR review comment**, it uses the PR’s head branch. @@ -70,9 +79,9 @@ Do user request uses the **same** verify commands as autofix: **`bugbot-fix-veri | Use case | What to do | |----------|------------| | Fix one or more **reported Bugbot findings** | Comment “fix it”, “fix all”, or refer to specific findings → **Autofix**. | -| Ask for a **general change** (test, refactor, feature, docs) | Comment with the request in natural language → **Do user request**. | +| Ask for a **general change** (test, refactor, feature, docs) | Comment with the request in natural language or use `/copilot implement ` → **Do user request**. | -Intent is inferred by the configured analysis role from the comment text and the list of unresolved findings; you don’t need to tag or label the comment. +Intent is inferred by the configured analysis role from the comment text and the list of unresolved findings; you don’t need to tag or label the comment. A natural-language request must mention the configured bot user (`@vypbot` by default) to invoke the agent. Explicit commands do not require a mention. --- diff --git a/docs/bugbot/examples.mdx b/docs/bugbot/examples.mdx index 3ac33a4b..75a22dd1 100644 --- a/docs/bugbot/examples.mdx +++ b/docs/bugbot/examples.mdx @@ -191,7 +191,7 @@ These are examples of comments that typically trigger **Bugbot autofix** (fix on | `please fix the null reference and the off-by-one` | Fix findings that match those descriptions. | | `fix finding xyz-123` | Fix the finding with id `xyz-123` if it exists and is unresolved. | -Post these on the **issue** or on the **PR** (or reply in the **review thread** of a finding). The action will run only if you have permission (org member or repo owner) and, for issue comments, there is an open PR for the issue. +Post these on the **issue** or on the **PR** (or reply in the **review thread** of a finding). The action will run only if you have permission (organization member, or repository owner / `push`/`maintain`/`admin` collaborator in a personal repository) and, for issue comments, there is an open PR for the issue. --- @@ -207,7 +207,7 @@ These are examples of comments that typically trigger **do user request** (gener | `implement the missing validation in the form` | Add validation logic. | | `add error handling for the API call` | Wrap or extend the API call with error handling. | -Same permission and workflow requirements as autofix: `contents: write` and org member or repo owner. +Same permission and workflow requirements as autofix: `contents: write` and an organization member, or repository owner / `push`/`maintain`/`admin` collaborator in a personal repository. Natural-language requests should mention `@vypbot`; use `/copilot implement ` when you want an explicit command. --- diff --git a/docs/bugbot/how-it-works.mdx b/docs/bugbot/how-it-works.mdx index 6d744ef6..394cd741 100644 --- a/docs/bugbot/how-it-works.mdx +++ b/docs/bugbot/how-it-works.mdx @@ -16,6 +16,7 @@ This page describes the **internal flow** of Bugbot: how detection runs, how the | **Push** to branch (or run `detect_potential_problems_action`) | Detection: load context → configured agent (findings + resolved ids) → filter → mark resolved → publish | New/updated comments on issue and PR; resolved threads on PR. | | **Comment** “fix it” / “fix all” (with permission) | Intent (analysis role) → Autofix (execution role) → verify commands → commit & push → mark findings resolved | Code change on branch; those findings marked resolved. | | **Comment** “add a test for X” (with permission) | Intent (analysis role) → Do user request (execution role) → verify commands → commit & push | Code change on branch. | +| **Comment** “analyze this PR for vulnerabilities” | Review (analysis/detection role) | Read-only findings; no file changes. | | **Comment** without permission or not a fix/do request | Think (analysis role) | Answer in comment; no file changes. | --- @@ -47,7 +48,7 @@ This page describes the **internal flow** of Bugbot: how detection runs, how the --- -## Fix intent and file-modifying actions (comment on issue or PR) +## Comment intent and file-modifying actions (comment on issue or PR) When you post a comment on an **issue** or **pull request** (or reply in a PR review thread), the action runs **intent detection** before doing anything that modifies files. @@ -58,8 +59,9 @@ When you post a comment on an **issue** or **pull request** (or reply in a PR re - **is_fix_request:** whether you are asking to fix one or more findings. - **target_finding_ids:** which finding ids to fix (if any). - **is_do_request:** whether you are asking for a general code change (not tied to findings). + - **is_review_request:** whether you are asking for a read-only review or vulnerability analysis. -2. **Permission check:** The action checks if the **comment author** is allowed to modify files: **organization member** (for org repos) or **repository owner** (for user repos). If not, it does **not** run autofix or do-user-request; it can still run **Think** to answer. +2. **Permission check:** The action checks if the **comment author** is allowed to modify files: **organization member** (for org repos), or **repository owner / collaborator with `push`, `maintain`, or `admin` permission** (for personal repos). If not, it does **not** run autofix or do-user-request; it can still run **Think** or a read-only review. 3. **Branch resolution (issue comment only):** On **issue_comment**, the action needs a **branch** to checkout and push to. It looks up an **open PR** that references the issue and uses that PR’s **head branch**. If there is no such PR, autofix and do-user-request are skipped. @@ -75,7 +77,9 @@ When you post a comment on an **issue** or **pull request** (or reply in a PR re - Call the configured execution role; it applies the changes. - Same **verify** and **commit/push** flow, with message `chore(#N): apply user request` or `chore: apply user request`. -6. **Think (when no file-modifying action ran):** If the comment was not a fix/do request or the user was not allowed, the action runs **Think** (analysis role) and posts an **answer** as a comment (e.g. explanation or suggestion), without editing files. +6. **Review:** If the bot is mentioned and intent detection classifies the request as a review, the action runs the read-only Bugbot detection flow. It never edits files or commits. + +7. **Think (when no file-modifying action ran):** If the comment was not a fix/do/review request or the user was not allowed, the action runs **Think** (analysis role) and posts an **answer** as a comment (e.g. explanation or suggestion), without editing files. --- diff --git a/docs/bugbot/permissions.mdx b/docs/bugbot/permissions.mdx index b97f5449..fdd05573 100644 --- a/docs/bugbot/permissions.mdx +++ b/docs/bugbot/permissions.mdx @@ -8,7 +8,7 @@ description: Minimum permissions and authorization rules for detection, autofix, | --- | --- | --- | | Detection and issue comments | `issues: write` | Workflow policy | | PR review comments | `pull-requests: write` | Workflow policy | -| Autofix commit/push | `contents: write` | Organization member or repository owner | -| User-request changes | `contents: write` | Organization member or repository owner | +| Autofix commit/push | `contents: write` | Organization member; or repository owner / `push`, `maintain`, or `admin` collaborator in personal repositories | +| User-request changes | `contents: write` | Organization member; or repository owner / `push`, `maintain`, or `admin` collaborator in personal repositories | Detection SHOULD run read-only where possible. Write permissions MUST be granted only to the event-specific job that needs them. See [Trust boundaries](/security-operations/security/trust-boundaries). diff --git a/docs/features.mdx b/docs/features.mdx index 8812e38a..1812c612 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -110,9 +110,9 @@ Codex is the default runtime for the repository's AI feature paths. OpenCode rem |--------|----------------|-------------| | **Check progress** | Push (commit) pipeline; optional single action `check_progress_action` / CLI `check-progress` | On every push, the configured agent compares issue vs branch diff and updates the progress label on the issue and on any open PRs for that branch. | | **Bugbot (potential problems)** | Push (commit) pipeline; optional single action `detect_potential_problems_action` / CLI `detect-potential-problems` | Analyzes branch vs base and posts findings as **comments on the issue** and **review comments on open PRs**; updates issue comments and marks PR review threads as resolved when findings are fixed. Configurable: `bugbot-severity`, `ai-ignore-files`. | -| **Do user request** | Issue comment; PR review comment | When you comment asking to perform a change in the repo (e.g. "add a test for X", "refactor this", "implement feature Y"), the configured agent applies the changes in the workspace, runs verify commands, and the action commits and pushes with a generic message. Same permission as Bugbot autofix: **only org members or the repo owner**. Uses the same `bugbot-fix-verify-commands` and agent CLI setup. | +| **Do user request** | Issue comment; PR review comment | When you comment asking to perform a change in the repo (or use `/copilot implement `), the configured agent applies the changes in the workspace, runs verify commands, and the action commits and pushes with a generic message. Organization repositories require an org member; personal repositories require the owner or a `push`/`maintain`/`admin` collaborator. Uses the same `bugbot-fix-verify-commands` and agent CLI setup. | | **Think / reasoning** | Issue/PR comment pipeline; single action `think_action` | Deep code analysis and change proposals (configured agent CLI). On comments: answers when mentioned (or on any comment for question/help issues). Runs when the comment was not a fix/do request or when the user is not allowed to trigger file-modifying actions. | -| **Explicit Copilot commands** | Issue and PR comments | `/copilot plan`, `/copilot clarify`, `/copilot estimate`, `/copilot test-plan`, `/copilot status`, `/copilot review`, `/copilot findings`, `/copilot recheck`, `/copilot fix`, and `/copilot dismiss` provide a bounded, predictable interface. | +| **Explicit Copilot commands** | Issue and PR comments | `/copilot help`, `/copilot plan`, `/copilot clarify`, `/copilot estimate`, `/copilot test-plan`, `/copilot explain`, `/copilot diagnose`, `/copilot analyze`, `/copilot status`, `/copilot review`, `/copilot findings`, `/copilot recheck`, `/copilot fix`, `/copilot dismiss`, and `/copilot implement` provide a bounded, predictable interface. | | **Comment translation** | Issue comment; PR review comment | Translates comments to the configured locale (`issues-locale`, `pull-requests-locale`) when they are written in another language. | | **AI PR description** | Pull request pipeline | Fills the repo's `.github/pull_request_template.md` from issue and branch diff (configured agent CLI). | | **Copilot** | CLI `copilot do` | Code analysis and file edits via the configured agent runtime. | diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx new file mode 100644 index 00000000..42285443 --- /dev/null +++ b/docs/issues/comment-commands.mdx @@ -0,0 +1,74 @@ +--- +title: Comment commands +description: Use @vypbot from issues and pull requests for help, analysis, planning, and authorized implementation. +--- + +# Comment commands + +Copilot can act as a project assistant directly from an issue or pull-request comment. With the default workflow PAT, the bot user is `@vypbot`; if your installation uses another account, replace that mention with the configured PAT user. + +## Fast start + +Comment this on an issue or PR: + +```text +@vypbot analyze this change for security and reliability risks +``` + +This starts a read-only analysis when the mention matches the bot account. It does not edit files, commit, or push. You can write the request in English, Spanish, or another language supported by the configured locale/agent flow. + +## Explicit commands + +Commands are recognized at the beginning of a comment, case-insensitively. They are deterministic and easier to audit in automation logs. + +| Command | Purpose | Writes files? | +| --- | --- | --- | +| `/copilot help` | Show this command reference. | No | +| `/copilot plan` | Propose an implementation plan. | No | +| `/copilot clarify` | Identify missing information and questions. | No | +| `/copilot estimate` | Estimate implementation effort. | No | +| `/copilot test-plan` | Propose a test strategy. | No | +| `/copilot explain` | Explain the relevant code or behavior. | No | +| `/copilot diagnose` | Investigate a reported error or failure. | No | +| `/copilot analyze` | Run a fresh Bugbot-style review for potential problems, including security risks. | No | +| `/copilot review` | Run the read-only review flow. | No | +| `/copilot findings` | Reconcile and publish current findings. | No | +| `/copilot recheck` | Re-run the review against the current branch. | No | +| `/copilot status` | Show the current Copilot status. | No | +| `/copilot description` | Refresh the PR description when that action is configured. | No | +| `/copilot fix ` | Fix one or more existing Bugbot findings. | Yes, if authorized | +| `/copilot dismiss ` | Dismiss one or more findings. | No file changes | +| `/copilot implement ` | Apply a general code, test, refactoring, or documentation request. | Yes, if authorized | + +`/copilot fix`, `/copilot dismiss`, and `/copilot implement` require arguments. The bot never treats a missing argument as permission to make a broad change. + +## Natural-language requests + +Mention the bot and describe the outcome you want: + +```text +@vypbot explain why this test is flaky +@vypbot diagnose the 500 error described above +@vypbot review the PR for authentication and injection vulnerabilities +@vypbot add regression tests for the null response case +``` + +The first three are read-only requests. The last one may be classified as a general implementation request. Natural-language file changes require an authorized actor and an available branch; the action runs the configured verification commands before committing and pushing. + +## Authorization and safety + +- Read-only commands and answers can be requested by any participant who can comment. +- File-modifying requests require the comment author to be an organization member for organization-owned repositories. +- In personal repositories, the author must be the repository owner or a collaborator with `push`, `maintain`, or `admin` permission. +- `/copilot dismiss` does not edit files, but it changes finding state and therefore uses the same authorization guard. +- An issue comment that changes files also needs an open PR referencing the issue so the action has a branch to update. PR comments use the PR head branch. +- The workflow must grant `contents: write` for file-modifying actions. Verification failures prevent the commit. +- Bot mentions are matched case-insensitively and with username boundaries, so `@vypbot-extra` does not accidentally trigger `@vypbot`. + +## New-issue welcome + +When a new issue is opened, Copilot adds one contextual welcome message. For question/help issues it is combined with the initial answer; for other issue types it is combined with the recommendation when available. If no recommendation can run, the static welcome still explains how to use `/copilot help` and `@vypbot`. + +The welcome contains a hidden marker so later workflow changes can recognize it without relying on the visible wording. + +See [Bugbot detection](/bugbot/detection), [Autofix](/bugbot/autofix), and [Do user request](/bugbot/do-user-request) for the execution details. diff --git a/docs/issues/index.mdx b/docs/issues/index.mdx index 90a10767..d1ae6f27 100644 --- a/docs/issues/index.mdx +++ b/docs/issues/index.mdx @@ -29,6 +29,9 @@ Copilot automates **issue tracking** so that labels, branch creation, project li Full workflow YAML and label examples. + + Ask @vypbot for help, analysis, diagnosis, plans, or authorized implementation from an issue or PR. + ## Quick summary @@ -72,6 +75,6 @@ Existing legacy labels such as `copilot:state:ready` are intentionally not migra ## Daily agent workflow -Use explicit commands when you want a reproducible action from a comment: `/copilot plan`, `/copilot test-plan`, `/copilot review`, `/copilot findings`, or `/copilot recheck`. For a reported finding, use `/copilot fix FINDING-ID` or `/copilot dismiss FINDING-ID`. A repeated review reconciles the existing finding marker and updates the same publication instead of creating a second copy. +Use [comment commands](/issues/comment-commands) when you want a reproducible action from an issue or PR. `/copilot help` shows the complete catalog. For a read-only analysis, use `/copilot analyze` (or `/copilot review`, `/copilot findings`, or `/copilot recheck`). For a reported finding, use `/copilot fix FINDING-ID` or `/copilot dismiss FINDING-ID`; for a general change, use `/copilot implement `. You can also mention `@vypbot` and describe the request in natural language. A repeated review reconciles the existing finding marker and updates the same publication instead of creating a second copy. For **step-by-step flows** per issue type (branch naming, source branch, deploy), see the issue type pages: [Feature](/issues/type/feature), [Bugfix](/issues/type/bugfix), [Docs](/issues/type/docs), [Chore](/issues/type/chore), [Hotfix](/issues/type/hotfix), [Release](/issues/type/release). diff --git a/setup/ISSUE_TEMPLATE/help_request.yml b/setup/ISSUE_TEMPLATE/help_request.yml index 9f9a3dd0..190f2444 100644 --- a/setup/ISSUE_TEMPLATE/help_request.yml +++ b/setup/ISSUE_TEMPLATE/help_request.yml @@ -17,6 +17,11 @@ body: value: | --- + - type: markdown + attributes: + value: | + **Copilot assistance:** After opening this issue, mention `@vypbot` in a comment to ask for an explanation, diagnosis, or read-only code/security analysis. Use `/copilot help` to see all available commands. + - type: dropdown id: help_area attributes: diff --git a/src/application/policies/__tests__/copilot_interaction_policy.test.ts b/src/application/policies/__tests__/copilot_interaction_policy.test.ts new file mode 100644 index 00000000..0a61766d --- /dev/null +++ b/src/application/policies/__tests__/copilot_interaction_policy.test.ts @@ -0,0 +1,38 @@ +import { + buildCopilotHelpMessage, + buildCopilotWelcomeMessage, + buildCopilotWelcomeResult, + COPILOT_WELCOME_MARKER, + normalizeCopilotBotUsername, +} from '../copilot_interaction_policy'; + +describe('Copilot interaction policy', () => { + it('normalizes safe GitHub bot usernames and falls back safely', () => { + expect(normalizeCopilotBotUsername('@VYPBOT')).toBe('VYPBOT'); + expect(normalizeCopilotBotUsername('not a username')).toBe('vypbot'); + expect(normalizeCopilotBotUsername(undefined)).toBe('vypbot'); + }); + + it('renders the supported command reference', () => { + const help = buildCopilotHelpMessage('vypbot'); + expect(help).toContain('/copilot help'); + expect(help).toContain('/copilot analyze'); + expect(help).toContain('/copilot implement '); + expect(help).toContain('@vypbot'); + }); + + it('renders a marked one-time issue welcome message', () => { + const welcome = buildCopilotWelcomeMessage('vypbot'); + expect(welcome.startsWith(COPILOT_WELCOME_MARKER)).toBe(true); + expect(welcome).toContain('Hi! I’m **@vypbot**'); + expect(welcome).toContain('/copilot help'); + }); + + it('builds a publishable markdown welcome result', () => { + expect(buildCopilotWelcomeResult('vypbot')).toMatchObject({ + id: 'CopilotWelcomeUseCase', + stepFormat: 'markdown', + executed: true, + }); + }); +}); diff --git a/src/application/policies/copilot_interaction_policy.ts b/src/application/policies/copilot_interaction_policy.ts new file mode 100644 index 00000000..dae4ba7f --- /dev/null +++ b/src/application/policies/copilot_interaction_policy.ts @@ -0,0 +1,70 @@ +import { Result } from '../../data/model/result'; + +export const DEFAULT_COPILOT_BOT_USERNAME = 'vypbot'; +export const COPILOT_WELCOME_MARKER = ''; + +const SAFE_GITHUB_USERNAME = /^[A-Za-z0-9-]+$/u; + +/** Keeps the bot identity safe when it is rendered into a GitHub comment. */ +export function normalizeCopilotBotUsername(username: string | undefined): string { + const candidate = username?.trim().replace(/^@/u, ''); + return candidate && SAFE_GITHUB_USERNAME.test(candidate) + ? candidate + : DEFAULT_COPILOT_BOT_USERNAME; +} + +/** Renders the stable command reference used by /copilot help. */ +export function buildCopilotHelpMessage(username?: string): string { + const bot = normalizeCopilotBotUsername(username); + return `## Copilot commands + +I’m **@${bot}**, the repository assistant. Use these commands on an issue or pull request: + +### Read-only + +- \`/copilot help\` — show this command reference. +- \`/copilot plan\` — propose an implementation plan. +- \`/copilot clarify\` — identify missing information and assumptions. +- \`/copilot estimate\` — estimate scope and complexity. +- \`/copilot test-plan\` — propose a focused testing strategy. +- \`/copilot explain \` — explain code or behavior. +- \`/copilot diagnose\` — investigate a reported problem and suggest likely causes. +- \`/copilot analyze\` — review the current issue, branch, or pull request for potential problems. +- \`/copilot review\` — run the Bugbot review. +- \`/copilot findings\` — show potential findings from the current code. +- \`/copilot recheck\` — re-run the review and reconcile findings. +- \`/copilot description\` — refresh the pull-request description. +- \`/copilot status\` — show the current automation status. + +### Changes + +- \`/copilot fix \` — fix one reported finding. +- \`/copilot fix all\` — fix all unresolved findings. +- \`/copilot dismiss \` — dismiss a finding. +- \`/copilot implement \` — apply an explicitly requested repository change. + +You can also ask a question in natural language by mentioning **@${bot}**. File-changing commands are restricted to authorized maintainers, run the configured checks, and report the resulting changes.`; +} + +/** Renders the one-time onboarding comment for a newly created issue. */ +export function buildCopilotWelcomeMessage(username?: string): string { + const bot = normalizeCopilotBotUsername(username); + return `${COPILOT_WELCOME_MARKER} + +Hi! I’m **@${bot}**, the Copilot assistant for this repository. + +I can answer questions, explain the codebase, propose implementation and test plans, review issues and pull requests for potential bugs or security problems, and help authorized maintainers apply changes. + +Try \`/copilot help\` to see the available commands, or mention **@${bot}** with your question.`; +} + +/** Creates a publishable result for issues that have no agent-generated reply. */ +export function buildCopilotWelcomeResult(username?: string): Result { + return new Result({ + id: 'CopilotWelcomeUseCase', + success: true, + executed: true, + stepFormat: 'markdown', + steps: [buildCopilotWelcomeMessage(username)], + }); +} diff --git a/src/application/ports/actor_authorization_ports.ts b/src/application/ports/actor_authorization_ports.ts index cdd33510..cbc7d5c8 100644 --- a/src/application/ports/actor_authorization_ports.ts +++ b/src/application/ports/actor_authorization_ports.ts @@ -1,3 +1,3 @@ export interface ActorAuthorizationPort { - isActorAllowedToModifyFiles(owner: string, actor: string, token: string): Promise; + isActorAllowedToModifyFiles(owner: string, repository: string, actor: string, token: string): Promise; } diff --git a/src/application/usecases/__tests__/comment_automation_action_workflow.test.ts b/src/application/usecases/__tests__/comment_automation_action_workflow.test.ts new file mode 100644 index 00000000..0199a1ce --- /dev/null +++ b/src/application/usecases/__tests__/comment_automation_action_workflow.test.ts @@ -0,0 +1,37 @@ +import { runCommentAutomationAction } from '../comment_automation_action_workflow'; + +function options(overrides: Record = {}) { + return { + taskId: 'CommentAutomation', + userComment: '@vypbot analyze this', + ...overrides, + } as never; +} + +describe('runCommentAutomationAction', () => { + it('fails clearly when the read-only review route is not composed', async () => { + const results = await runCommentAutomationAction( + {} as never, + options(), + 'review', + undefined, + {} as never, + ); + + expect(results[0]).toMatchObject({ + id: 'CommentAutomation.Review', + success: false, + executed: false, + }); + }); + + it('does not perform an action for the think route', async () => { + await expect(runCommentAutomationAction( + {} as never, + options(), + 'think', + undefined, + {} as never, + )).resolves.toEqual([]); + }); +}); diff --git a/src/application/usecases/__tests__/comment_automation_route_policy.test.ts b/src/application/usecases/__tests__/comment_automation_route_policy.test.ts index 972b5fd9..57cb2898 100644 --- a/src/application/usecases/__tests__/comment_automation_route_policy.test.ts +++ b/src/application/usecases/__tests__/comment_automation_route_policy.test.ts @@ -2,14 +2,20 @@ import { resolveCommentAutomationRoute } from '../comment_automation_route_polic const fixPayload = { isFixRequest: true, targetFindingIds: ['finding'], context: { issueNumber: 1 } } as never; const doPayload = { isDoRequest: true } as never; +const reviewPayload = { isReviewRequest: true } as never; describe('comment automation route policy', () => { it('routes authorized fix requests to autofix', () => { - expect(resolveCommentAutomationRoute(fixPayload, true)).toBe('autofix'); + expect(resolveCommentAutomationRoute(fixPayload, true, true)).toBe('autofix'); }); it('routes authorized user requests to do-user-request', () => { - expect(resolveCommentAutomationRoute(doPayload, true)).toBe('do-user-request'); + expect(resolveCommentAutomationRoute(doPayload, true, true)).toBe('do-user-request'); + }); + + it('routes a mentioned review request to the read-only review route', () => { + expect(resolveCommentAutomationRoute(reviewPayload, false, true)).toBe('review'); + expect(resolveCommentAutomationRoute(reviewPayload, true, false)).toBe('think'); }); it('routes unauthorized file modifications to think', () => { @@ -17,6 +23,14 @@ describe('comment automation route policy', () => { expect(resolveCommentAutomationRoute(doPayload, false)).toBe('think'); }); + it('does not route an unmentioned natural-language request to a file-changing action', () => { + expect(resolveCommentAutomationRoute(doPayload, true, false)).toBe('think'); + }); + + it('allows an explicit mutation command without a bot mention', () => { + expect(resolveCommentAutomationRoute(doPayload, true, false, true)).toBe('do-user-request'); + }); + it('routes non-modifying comments to think', () => { expect(resolveCommentAutomationRoute(undefined, true)).toBe('think'); }); diff --git a/src/application/usecases/__tests__/comment_automation_use_case.test.ts b/src/application/usecases/__tests__/comment_automation_use_case.test.ts index f09ed87e..cd8c310f 100644 --- a/src/application/usecases/__tests__/comment_automation_use_case.test.ts +++ b/src/application/usecases/__tests__/comment_automation_use_case.test.ts @@ -48,6 +48,7 @@ describe("runCommentAutomation", () => { owner: "o", repo: "r", actor: "actor", + tokenUser: "vypbot", tokens: { token: "t" }, } as Execution, { @@ -72,7 +73,7 @@ describe("runCommentAutomation", () => { taskId: "do-user-request", invoke: jest.fn().mockResolvedValue([]), }, - userComment: "fix it", + userComment: "@vypbot fix it", gitCommitPort: {} as never, }, { @@ -177,6 +178,45 @@ describe("runCommentAutomation", () => { expect(intent.invoke).not.toHaveBeenCalled(); }); + it('returns static help without invoking language or intent agents', async () => { + const language = { invoke: jest.fn() }; + const intent = { invoke: jest.fn() }; + const think = { invoke: jest.fn() }; + + const results = await runCommentAutomation( + { + owner: 'o', + repo: 'r', + actor: 'actor', + tokenUser: 'vypbot', + tokens: { token: 't' }, + } as Execution, + { + taskId: 'CommentAutomation', + languageUseCase: language as never, + intentUseCase: intent as never, + thinkUseCase: think as never, + autofixUseCase: {} as never, + doUserRequestUseCase: {} as never, + userComment: '/copilot help', + gitCommitPort: {} as never, + }, + {} as never, + {} as never, + {} as never, + ); + + expect(results[0]).toMatchObject({ + id: 'CommentAutomation.Help', + success: true, + stepFormat: 'markdown', + }); + expect(results[0].steps[0]).toContain('@vypbot'); + expect(language.invoke).not.toHaveBeenCalled(); + expect(intent.invoke).not.toHaveBeenCalled(); + expect(think.invoke).not.toHaveBeenCalled(); + }); + it('routes explicit review commands to the read-only Bugbot review use case', async () => { const review = { invoke: jest.fn().mockResolvedValue([successfulResult('review')]) }; const think = { invoke: jest.fn() }; @@ -203,6 +243,107 @@ describe("runCommentAutomation", () => { expect(think.invoke).not.toHaveBeenCalled(); }); + it('reports when an explicit analysis command has no review composition', async () => { + const results = await runCommentAutomation( + { owner: 'o', repo: 'r', actor: 'actor', tokens: { token: 't' } } as Execution, + { + taskId: 'CommentAutomation', + languageUseCase: {} as never, + intentUseCase: {} as never, + thinkUseCase: {} as never, + autofixUseCase: {} as never, + doUserRequestUseCase: {} as never, + userComment: '/copilot analyze', + gitCommitPort: {} as never, + }, + {} as never, + {} as never, + {} as never, + ); + + expect(results.at(-1)).toMatchObject({ + id: 'CommentAutomation.Review', + success: false, + executed: true, + }); + }); + + it('routes a mentioned natural-language analysis request to the read-only review use case', async () => { + const review = { invoke: jest.fn().mockResolvedValue([successfulResult('review')]) }; + const intent = { + invoke: jest.fn().mockResolvedValue([successfulResult('intent', { + isFixRequest: false, + isDoRequest: false, + isReviewRequest: true, + targetFindingIds: [], + })]), + }; + const think = { invoke: jest.fn() }; + + const results = await runCommentAutomation( + { + owner: 'o', + repo: 'r', + actor: 'actor', + tokenUser: 'vypbot', + tokens: { token: 't' }, + } as Execution, + { + taskId: 'CommentAutomation', + languageUseCase: { invoke: jest.fn().mockResolvedValue([]) } as never, + intentUseCase: intent as never, + thinkUseCase: think as never, + autofixUseCase: {} as never, + doUserRequestUseCase: {} as never, + reviewPotentialProblemsUseCase: review as never, + userComment: '@VYPBOT analyze the changes for security issues', + gitCommitPort: {} as never, + }, + { isActorAllowedToModifyFiles: jest.fn().mockResolvedValue(false) } as never, + {} as never, + {} as never, + ); + + expect(results.map(result => result.id)).toEqual(['intent', 'review']); + expect(review.invoke).toHaveBeenCalledTimes(1); + expect(think.invoke).not.toHaveBeenCalled(); + }); + + it('keeps an explicit implement request on the authorized mutation route', async () => { + const doUserRequest = { invoke: jest.fn().mockResolvedValue([]) }; + const intent = { + invoke: jest.fn().mockResolvedValue([successfulResult('intent', { + isFixRequest: false, + isDoRequest: true, + isReviewRequest: false, + targetFindingIds: [], + requestText: 'add a regression test', + })]), + }; + + const results = await runCommentAutomation( + { owner: 'o', repo: 'r', actor: 'actor', tokens: { token: 't' } } as Execution, + { + taskId: 'CommentAutomation', + languageUseCase: { invoke: jest.fn().mockResolvedValue([]) } as never, + intentUseCase: intent as never, + thinkUseCase: { invoke: jest.fn() } as never, + autofixUseCase: {} as never, + doUserRequestUseCase: doUserRequest as never, + userComment: '/copilot implement add a regression test', + gitCommitPort: {} as never, + }, + { isActorAllowedToModifyFiles: jest.fn().mockResolvedValue(true) } as never, + {} as never, + {} as never, + ); + + expect(doUserRequest.invoke).toHaveBeenCalledWith(expect.objectContaining({ + userComment: 'add a regression test', + })); + expect(results).toContainEqual(expect.objectContaining({ id: 'intent' })); + }); + it('routes explicit PR description commands without language or intent detection', async () => { const description = { invokeExplicit: jest.fn().mockResolvedValue([successfulResult('description')]) }; const language = { invoke: jest.fn() }; @@ -231,6 +372,31 @@ describe("runCommentAutomation", () => { expect(intent.invoke).not.toHaveBeenCalled(); }); + it('reports when an explicit PR description command is unavailable', async () => { + const results = await runCommentAutomation( + { owner: 'o', repo: 'r', actor: 'actor', tokens: { token: 't' } } as Execution, + { + taskId: 'CommentAutomation', + languageUseCase: {} as never, + intentUseCase: {} as never, + thinkUseCase: {} as never, + autofixUseCase: {} as never, + doUserRequestUseCase: {} as never, + userComment: '/copilot description', + gitCommitPort: {} as never, + }, + {} as never, + {} as never, + {} as never, + ); + + expect(results.at(-1)).toMatchObject({ + id: 'CommentAutomation.Description', + success: false, + executed: false, + }); + }); + it('rejects an invalid explicit command without invoking an agent', async () => { const think = { invoke: jest.fn() }; const results = await runCommentAutomation( @@ -278,4 +444,29 @@ describe("runCommentAutomation", () => { expect(results).toEqual([expect.objectContaining({ id: 'dismiss' })]); expect(dismiss.invoke).toHaveBeenCalledWith(expect.objectContaining({ findingIds: ['FINDING-1'] })); }); + + it('skips explicit dismiss commands when the actor is not authorized', async () => { + const authorization = { isActorAllowedToModifyFiles: jest.fn().mockResolvedValue(false) }; + const dismiss = { invoke: jest.fn() }; + const results = await runCommentAutomation( + { owner: 'o', repo: 'r', actor: 'actor', tokens: { token: 't' } } as Execution, + { + taskId: 'CommentAutomation', + languageUseCase: {} as never, + intentUseCase: {} as never, + thinkUseCase: {} as never, + autofixUseCase: {} as never, + doUserRequestUseCase: {} as never, + dismissBugbotFindingsUseCase: dismiss as never, + userComment: '/copilot dismiss FINDING-1', + gitCommitPort: {} as never, + }, + authorization as never, + {} as never, + {} as never, + ); + + expect(dismiss.invoke).not.toHaveBeenCalled(); + expect(results[0]).toMatchObject({ success: true, executed: false }); + }); }); diff --git a/src/application/usecases/__tests__/issue_comment_use_case.test.ts b/src/application/usecases/__tests__/issue_comment_use_case.test.ts index c8860264..3f5c39d7 100644 --- a/src/application/usecases/__tests__/issue_comment_use_case.test.ts +++ b/src/application/usecases/__tests__/issue_comment_use_case.test.ts @@ -537,6 +537,7 @@ describe("IssueCommentUseCase", () => { expect(mockIsActorAllowedToModifyFiles).toHaveBeenCalledTimes(1); expect(mockIsActorAllowedToModifyFiles).toHaveBeenCalledWith( "o", + "r", undefined, "t", ); diff --git a/src/application/usecases/__tests__/issue_use_case.test.ts b/src/application/usecases/__tests__/issue_use_case.test.ts index af2d64f0..aa98b8f0 100644 --- a/src/application/usecases/__tests__/issue_use_case.test.ts +++ b/src/application/usecases/__tests__/issue_use_case.test.ts @@ -170,6 +170,39 @@ describe("IssueUseCase", () => { expect(mockRecommendStepsInvoke).not.toHaveBeenCalled(); }); + it("posts a static welcome for a newly opened issue when no AI recommendation applies", async () => { + const param = minimalExecution({ + tokenUser: "vypbot", + eventName: "issues", + inputs: { eventName: "issues", action: "opened" }, + issue: { opened: true }, + labels: { isRelease: true, isQuestion: false, isHelp: false }, + }); + + const results = await createUseCase().invoke(param); + + expect(mockRecommendStepsInvoke).not.toHaveBeenCalled(); + expect(results.some((result) => result.id === "CopilotWelcomeUseCase")).toBe(true); + expect(results.find((result) => result.id === "CopilotWelcomeUseCase")?.steps[0]).toContain( + "", + ); + }); + + it("posts a static welcome when the initial help agent cannot answer", async () => { + const param = minimalExecution({ + tokenUser: "vypbot", + eventName: "issues", + inputs: { eventName: "issues", action: "opened" }, + issue: { opened: true }, + labels: { isRelease: false, isQuestion: true, isHelp: false }, + }); + + const results = await createUseCase().invoke(param); + + expect(mockAnswerIssueHelpInvoke).toHaveBeenCalledWith(param); + expect(results.some((result) => result.id === "CopilotWelcomeUseCase")).toBe(true); + }); + it("answers help for a newly opened question or help issue", async () => { mockAnswerIssueHelpInvoke.mockResolvedValue([ new Result({ id: "help", success: true, executed: true, steps: [] }), diff --git a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts index 67f8b23d..322b5186 100644 --- a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts @@ -87,6 +87,23 @@ describe('RecommendStepsUseCase', () => { expect(getResultPayload(results[0].payload)?.recommendedSteps).toContain('1. Reproduce'); }); + it('includes the bot welcome in the first recommendation for a newly opened issue', async () => { + mockGetDescription.mockResolvedValue('Implement login feature.'); + mockAskAgent.mockResolvedValue('1. Add auth module'); + const param = baseParam({ + tokenUser: 'vypbot', + eventName: 'issues', + issue: { opened: true }, + inputs: { eventName: 'issues', action: 'opened' }, + }); + + const results = await useCase.invoke(param); + + expect(results[0].steps[0]).toContain(''); + expect(results[0].steps[0]).toContain('Hi! I’m **@vypbot**'); + expect(results[0].steps).toContain('## Recommended implementation steps'); + }); + it('removes Copilot metadata from the prompt and fingerprint input', async () => { mockGetDescription.mockResolvedValue('Implement login feature.\n\n'); mockAskAgent.mockResolvedValue('1. Add auth module'); diff --git a/src/application/usecases/actions/recommend_steps_result_policy.ts b/src/application/usecases/actions/recommend_steps_result_policy.ts index 6ff9f214..076f752f 100644 --- a/src/application/usecases/actions/recommend_steps_result_policy.ts +++ b/src/application/usecases/actions/recommend_steps_result_policy.ts @@ -3,6 +3,7 @@ import { Result } from '../../../data/model/result'; import type { RecommendationState } from '../../../data/model/recommendation_state'; import { createRecommendationFingerprint, isNoNewRecommendation, limitStoredRecommendation } from '../../../application/policies/recommendation_policy'; import { logDebugInfo, logError, logInfo } from '../../ports/logging_ports'; +import { buildCopilotWelcomeMessage } from '../../../application/policies/copilot_interaction_policy'; export function buildRecommendationResult( param: Execution, @@ -27,16 +28,23 @@ export function buildRecommendationResult( recommendationFingerprint, recommendation: limitStoredRecommendation(steps), }; + const stepsWithWelcome = isNewIssue(param) + ? [buildCopilotWelcomeMessage(param.tokenUser), '## Recommended implementation steps', steps] + : ['## Recommended implementation steps', steps]; return [new Result({ id: taskId, success: true, executed: true, stepFormat: 'markdown', - steps: ['## Recommended implementation steps', steps], + steps: stepsWithWelcome, payload: { issueNumber, recommendedSteps: steps, recommendationState }, })]; } +function isNewIssue(param: Execution): boolean { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} + function skipUnchangedRecommendation(param: Execution, previous: RecommendationState, fingerprint: string, reason: string): Result[] { param.currentConfiguration.recommendationState = { ...previous, issueDescriptionFingerprint: fingerprint }; logInfo(`RecommendSteps: ${reason}; skipping recommendation comment.`); diff --git a/src/application/usecases/comment_automation_action_workflow.ts b/src/application/usecases/comment_automation_action_workflow.ts index 2b129a86..96a32a2a 100644 --- a/src/application/usecases/comment_automation_action_workflow.ts +++ b/src/application/usecases/comment_automation_action_workflow.ts @@ -9,7 +9,7 @@ import { commitAutofixAndResolveFindings } from "./steps/commit/bugbot/commit_au import { commitUserRequestIfSuccessful } from "./steps/commit/bugbot/commit_user_request_workflow"; import { logInfo } from "../ports/logging_ports"; -export type CommentAutomationAction = "autofix" | "do-user-request" | "think"; +export type CommentAutomationAction = "autofix" | "do-user-request" | "review" | "think"; export interface CommentAutomationActionPorts { authenticatedUserPort: AuthenticatedUserPort; @@ -25,55 +25,86 @@ export async function runCommentAutomationAction( intentPayload: BugbotFixIntentPayload | undefined, ports: CommentAutomationActionPorts, ): Promise { - if (route === "autofix" && intentPayload) { - logInfo("Running bugbot autofix."); - const autofixResults = await options.autofixUseCase.invoke({ - execution: param, - targetFindingIds: intentPayload.targetFindingIds, - userComment: options.userComment, - context: intentPayload.context, - branchOverride: intentPayload.branchOverride, - }); - const resolutionErrors = await commitAutofixAndResolveFindings( - param, - intentPayload, - autofixResults, - ports.authenticatedUserPort, - ports.bugbotResolutionPorts, - ports.gitCommitPort, - ); - if (resolutionErrors.length > 0) { - autofixResults.push( - new Result({ - id: `${options.taskId}.AutofixPostflight`, - success: false, - executed: true, - steps: [ - "Autofix postflight failed: commit/push or finding reconciliation did not complete.", - ], - errors: resolutionErrors, - }), - ); - } - return autofixResults; + if (route === "review") return runReviewAction(param, options); + if (route === "autofix") return runAutofixAction(param, options, intentPayload, ports); + if (route === "do-user-request") return runDoUserRequestAction(param, options, intentPayload, ports); + return []; +} + +async function runReviewAction( + param: Execution, + options: CommentAutomationOptions, +): Promise { + if (!options.reviewPotentialProblemsUseCase) { + return [new Result({ + id: `${options.taskId}.Review`, + success: false, + executed: false, + errors: ["Read-only review is not available in this composition."], + })]; } + logInfo("Running natural-language read-only review."); + return options.reviewPotentialProblemsUseCase.invoke(param); +} - if (route === "do-user-request" && intentPayload) { - logInfo("Running do user request."); - const doResults = await options.doUserRequestUseCase.invoke({ - execution: param, - userComment: options.userComment, - branchOverride: intentPayload.branchOverride, - }); - const commitResults = await commitUserRequestIfSuccessful( - param, - intentPayload.branchOverride, - doResults, - ports.authenticatedUserPort, - ports.gitCommitPort, +async function runAutofixAction( + param: Execution, + options: CommentAutomationOptions, + intentPayload: BugbotFixIntentPayload | undefined, + ports: CommentAutomationActionPorts, +): Promise { + if (!intentPayload) return []; + logInfo("Running bugbot autofix."); + const autofixResults = await options.autofixUseCase.invoke({ + execution: param, + targetFindingIds: intentPayload.targetFindingIds, + userComment: options.userComment, + context: intentPayload.context, + branchOverride: intentPayload.branchOverride, + }); + const resolutionErrors = await commitAutofixAndResolveFindings( + param, + intentPayload, + autofixResults, + ports.authenticatedUserPort, + ports.bugbotResolutionPorts, + ports.gitCommitPort, + ); + if (resolutionErrors.length > 0) { + autofixResults.push( + new Result({ + id: `${options.taskId}.AutofixPostflight`, + success: false, + executed: true, + steps: [ + "Autofix postflight failed: commit/push or finding reconciliation did not complete.", + ], + errors: resolutionErrors, + }), ); - return [...doResults, ...commitResults]; } + return autofixResults; +} - return []; +async function runDoUserRequestAction( + param: Execution, + options: CommentAutomationOptions, + intentPayload: BugbotFixIntentPayload | undefined, + ports: CommentAutomationActionPorts, +): Promise { + if (!intentPayload) return []; + logInfo("Running do user request."); + const doResults = await options.doUserRequestUseCase.invoke({ + execution: param, + userComment: intentPayload.requestText?.trim() || options.userComment, + branchOverride: intentPayload.branchOverride, + }); + const commitResults = await commitUserRequestIfSuccessful( + param, + intentPayload.branchOverride, + doResults, + ports.authenticatedUserPort, + ports.gitCommitPort, + ); + return [...doResults, ...commitResults]; } diff --git a/src/application/usecases/comment_automation_command_workflow.ts b/src/application/usecases/comment_automation_command_workflow.ts index f2bd6e3e..6712237a 100644 --- a/src/application/usecases/comment_automation_command_workflow.ts +++ b/src/application/usecases/comment_automation_command_workflow.ts @@ -4,6 +4,7 @@ import type { ActorAuthorizationPort } from '../ports/actor_authorization_ports' import type { CommentAutomationOptions } from './comment_automation_contracts'; import type { ParsedCopilotCommand } from '../../domain/copilot_command'; import { buildCopilotStatusResult } from '../policies/status_command_policy'; +import { buildCopilotHelpMessage } from '../policies/copilot_interaction_policy'; /** Executes deterministic /copilot commands without routing them through intent detection. */ export async function runExplicitCommentCommand( @@ -12,14 +13,28 @@ export async function runExplicitCommentCommand( command: ParsedCopilotCommand, actorAuthorizationPort: ActorAuthorizationPort, ): Promise { + if (command.name === 'help') return runHelpCommand(param, options); if (command.name === 'status') return [buildCopilotStatusResult(param, options.taskId)]; if (command.name === 'dismiss') return runDismissCommand(param, options, command, actorAuthorizationPort); if (command.name === 'description') return runDescriptionCommand(param, options); - if (['review', 'findings', 'recheck'].includes(command.name)) return runReviewCommand(param, options, command); - if (command.name === 'fix') return undefined; + if (['analyze', 'review', 'findings', 'recheck'].includes(command.name)) return runReviewCommand(param, options, command); + if (command.name === 'fix' || command.name === 'implement') return undefined; return runThinkCommand(param, options, command); } +function runHelpCommand( + param: Execution, + options: CommentAutomationOptions, +): Result[] { + return [new Result({ + id: `${options.taskId}.Help`, + success: true, + executed: true, + stepFormat: 'markdown', + steps: [buildCopilotHelpMessage(param.tokenUser)], + })]; +} + async function runDescriptionCommand( param: Execution, options: CommentAutomationOptions, @@ -43,6 +58,7 @@ async function runDismissCommand( ): Promise { const allowed = await actorAuthorizationPort.isActorAllowedToModifyFiles( param.owner, + param.repo, param.actor, param.tokens.token, ); diff --git a/src/application/usecases/comment_automation_decision_workflow.ts b/src/application/usecases/comment_automation_decision_workflow.ts index 5a984fb1..d85d79ae 100644 --- a/src/application/usecases/comment_automation_decision_workflow.ts +++ b/src/application/usecases/comment_automation_decision_workflow.ts @@ -6,6 +6,8 @@ import { getBugbotFixIntentPayload } from "./steps/commit/bugbot/bugbot_fix_inte import { resolveCommentAutomationRoute, type CommentAutomationRoute } from "./comment_automation_route_policy"; import type { BugbotFixIntentPayload } from "./steps/commit/bugbot/bugbot_fix_intent_payload"; import type { CommentAutomationOptions } from "./comment_automation_contracts"; +import { containsBotMention } from './steps/common/think_input_policy'; +import { parseCopilotCommand } from '../../domain/copilot_command'; export interface CommentAutomationDecision { intentResults: Result[]; @@ -21,13 +23,19 @@ export async function resolveCommentAutomationDecision( logInfo("Running bugbot fix intent detection (before Think)."); const intentResults = await options.intentUseCase.invoke(param); const intentPayload = getBugbotFixIntentPayload(intentResults); + const parsedCommand = parseCopilotCommand(options.userComment); + const explicitMutationCommand = parsedCommand.kind === 'command' + && (parsedCommand.command.name === 'fix' || parsedCommand.command.name === 'implement'); const route = resolveCommentAutomationRoute( intentPayload, await actorAuthorizationPort.isActorAllowedToModifyFiles( param.owner, + param.repo, param.actor, param.tokens.token, ), + containsBotMention(options.userComment, param.tokenUser ?? ''), + explicitMutationCommand, ); logIntent(intentPayload); diff --git a/src/application/usecases/comment_automation_route_policy.ts b/src/application/usecases/comment_automation_route_policy.ts index 49662cf9..e19fc42f 100644 --- a/src/application/usecases/comment_automation_route_policy.ts +++ b/src/application/usecases/comment_automation_route_policy.ts @@ -1,12 +1,16 @@ import type { BugbotFixIntentPayload } from './steps/commit/bugbot/bugbot_fix_intent_payload'; import { canRunBugbotAutofix, canRunDoUserRequest } from './steps/commit/bugbot/bugbot_fix_intent_payload'; -export type CommentAutomationRoute = 'autofix' | 'do-user-request' | 'think'; +export type CommentAutomationRoute = 'autofix' | 'do-user-request' | 'review' | 'think'; export function resolveCommentAutomationRoute( payload: BugbotFixIntentPayload | undefined, allowedToModifyFiles: boolean, + botMentioned = false, + explicitMutationCommand = false, ): CommentAutomationRoute { + if (!botMentioned && !explicitMutationCommand) return 'think'; + if (botMentioned && payload?.isReviewRequest) return 'review'; if (!allowedToModifyFiles) return 'think'; if (canRunBugbotAutofix(payload)) return 'autofix'; if (canRunDoUserRequest(payload)) return 'do-user-request'; diff --git a/src/application/usecases/issue_workflow.ts b/src/application/usecases/issue_workflow.ts index 4d29b266..7cf39378 100644 --- a/src/application/usecases/issue_workflow.ts +++ b/src/application/usecases/issue_workflow.ts @@ -1,8 +1,9 @@ import type { Execution } from "../../data/model/execution"; -import { Result } from "../../data/model/result"; +import { getResultPayload, Result } from "../../data/model/result"; import { logError } from "../ports/logging_ports"; import type { ParamUseCase } from "./base/param_usecase"; import type { IssueWorkflowSteps } from "./issue_workflow_steps"; +import { buildCopilotWelcomeResult, COPILOT_WELCOME_MARKER } from '../policies/copilot_interaction_policy'; export interface IssueWorkflowPorts { recommendStepsUseCase: ParamUseCase; @@ -62,11 +63,28 @@ export async function runIssueWorkflow( const recommendation = resolveIssueRecommendation(param, ports); if (recommendation) { - results.push(...(await recommendation.invoke(param))); + const recommendationResults = await recommendation.invoke(param); + results.push(...recommendationResults); + if (isNewIssue(param) && !containsWelcome(recommendationResults)) { + results.push(buildCopilotWelcomeResult(param.tokenUser)); + } + } else if (isNewIssue(param)) { + results.push(buildCopilotWelcomeResult(param.tokenUser)); } return results; } +function containsWelcome(results: readonly Result[]): boolean { + return results.some((result) => + result.steps.some((step) => step.includes(COPILOT_WELCOME_MARKER)) + || getResultPayload(result.payload)?.welcomePublished === true, + ); +} + +function isNewIssue(param: Execution): boolean { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} + function resolveIssueRecommendation( param: Execution, ports: IssueWorkflowPorts, diff --git a/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_policy.test.ts b/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_policy.test.ts index ebf80265..27f1bf69 100644 --- a/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_policy.test.ts +++ b/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_policy.test.ts @@ -77,6 +77,7 @@ describe("detect bugbot fix intent policy", () => { ).toEqual({ isFixRequest: true, isDoRequest: false, + isReviewRequest: false, targetFindingIds: ["finding-1"], }); }); @@ -94,6 +95,7 @@ describe("detect bugbot fix intent policy", () => { ).toEqual({ isFixRequest: false, isDoRequest: true, + isReviewRequest: false, targetFindingIds: [], }); }); @@ -108,12 +110,13 @@ describe("detect bugbot fix intent policy", () => { it("defaults malformed fields to safe values", () => { expect( parseBugbotFixIntentResponse( - { is_fix_request: "true", is_do_request: 1, target_finding_ids: "finding-1" }, + { is_fix_request: "true", is_do_request: 1, is_review_request: false, target_finding_ids: "finding-1" }, unresolvedIds, ), ).toEqual({ isFixRequest: false, isDoRequest: false, + isReviewRequest: false, targetFindingIds: [], }); }); diff --git a/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_use_case.test.ts b/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_use_case.test.ts index c5a5cef7..fbe179a2 100644 --- a/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_use_case.test.ts +++ b/src/application/usecases/steps/commit/bugbot/__tests__/detect_bugbot_fix_intent_use_case.test.ts @@ -158,13 +158,41 @@ describe("DetectBugbotFixIntentUseCase", () => { ); }); - it("returns empty results when no unresolved findings", async () => { + it("still classifies a comment when no unresolved findings exist", async () => { mockLoadBugbotContext.mockResolvedValue(mockContextWithUnresolved(0)); + mockAskAgent.mockResolvedValue({ is_fix_request: false, target_finding_ids: [], is_do_request: true, is_review_request: false }); const results = await useCase.invoke(baseExecution()); - expect(results).toEqual([]); + expect(results).toHaveLength(1); + expect(mockAskAgent).toHaveBeenCalledTimes(1); + expect((results[0].payload as { isDoRequest: boolean }).isDoRequest).toBe(true); + }); + + it('classifies a natural-language read-only review request', async () => { + mockLoadBugbotContext.mockResolvedValue(mockContextWithUnresolved(0)); + mockAskAgent.mockResolvedValue({ is_fix_request: false, target_finding_ids: [], is_do_request: false, is_review_request: true }); + + const results = await useCase.invoke(baseExecution({ + issue: { ...baseExecution().issue, commentBody: '@bot analyze the changes for security issues' }, + } as Partial)); + + expect((results[0].payload as { isReviewRequest?: boolean }).isReviewRequest).toBe(true); + }); + + it('routes explicit implement commands without requiring intent model classification', async () => { + const context = mockContextWithUnresolved(0); + mockLoadBugbotContext.mockResolvedValue(context); + + const results = await useCase.invoke(baseExecution({ + issue: { ...baseExecution().issue, commentBody: '/copilot implement add a regression test' }, + } as Partial)); + expect(mockAskAgent).not.toHaveBeenCalled(); + expect(results[0].payload).toMatchObject({ + isDoRequest: true, + requestText: 'add a regression test', + }); }); it("calls askAgent and returns payload with filtered target ids", async () => { @@ -198,6 +226,17 @@ describe("DetectBugbotFixIntentUseCase", () => { expect((results[0].payload as { targetFindingIds: string[] }).targetFindingIds).toEqual(['finding-0']); }); + it('skips an explicit fix command when there are no unresolved findings', async () => { + mockLoadBugbotContext.mockResolvedValue(mockContextWithUnresolved(0)); + + const results = await useCase.invoke(baseExecution({ + issue: { ...baseExecution().issue, commentBody: '/copilot fix finding-0' }, + } as Partial)); + + expect(results).toEqual([]); + expect(mockAskAgent).not.toHaveBeenCalled(); + }); + it('supports selecting all unresolved findings with an explicit command', async () => { mockLoadBugbotContext.mockResolvedValue(mockContextWithUnresolved(2)); diff --git a/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.ts b/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.ts index dca35d97..69e84333 100644 --- a/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.ts +++ b/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.ts @@ -5,6 +5,8 @@ export interface BugbotFixIntent { isFixRequest: boolean; isDoRequest: boolean; targetFindingIds: string[]; + isReviewRequest?: boolean; + requestText?: string; } export interface BugbotCommentSources { @@ -53,6 +55,7 @@ export function parseBugbotFixIntentResponse( const payload = response as Record; const isFixRequest = payload.is_fix_request === true; const isDoRequest = payload.is_do_request === true; + const isReviewRequest = payload.is_review_request === true; const requestedIds = Array.isArray(payload.target_finding_ids) ? payload.target_finding_ids.filter((id): id is string => typeof id === "string") : []; @@ -61,7 +64,7 @@ export function parseBugbotFixIntentResponse( ? unique(requestedIds.filter((id) => unresolvedFindingIds.has(id))) : []; - return { isFixRequest, isDoRequest, targetFindingIds }; + return { isFixRequest, isDoRequest, targetFindingIds, isReviewRequest }; } function unique(values: readonly string[]): string[] { diff --git a/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.ts b/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.ts index a3a50e40..7780f19d 100644 --- a/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.ts +++ b/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.ts @@ -25,7 +25,7 @@ export interface DetectBugbotFixIntentWorkflowPorts { contextPorts: BugbotContextPorts; } -/** Detects whether a comment targets Bugbot findings and returns the validated intent payload. */ +/** Detects whether a comment requests a finding fix, repository change, or read-only review. */ export async function runDetectBugbotFixIntentWorkflow( param: Execution, ports: DetectBugbotFixIntentWorkflowPorts, @@ -45,7 +45,8 @@ export async function runDetectBugbotFixIntentWorkflow( const explicitCommand = parseCopilotCommand(commentBody); const isExplicitFix = explicitCommand.kind === 'command' && explicitCommand.command.name === 'fix'; - if (!isExplicitFix && !isAgentConfigurationReady(param.ai?.getAgentConfiguration("findings"))) { + const isExplicitImplement = explicitCommand.kind === 'command' && explicitCommand.command.name === 'implement'; + if (!isExplicitFix && !isExplicitImplement && !isAgentConfigurationReady(param.ai?.getAgentConfiguration("findings"))) { logInfo("Agent not configured; skipping bugbot fix intent detection."); return results; } @@ -64,15 +65,34 @@ export async function runDetectBugbotFixIntentWorkflow( : undefined; const context = await loadBugbotContext(param, contextOptions, ports.contextPorts); const unresolvedWithBody = context.unresolvedFindingsWithBody ?? []; - if (unresolvedWithBody.length === 0) { - logInfo("No unresolved bugbot findings for this issue/PR; skipping bugbot fix intent detection."); - return results; - } const unresolvedIds = new Set(unresolvedWithBody.map((finding) => finding.id)); const unresolvedFindings = buildUnresolvedFindingSummaries(unresolvedWithBody); const parentCommentBody = await resolveParentCommentBody(param, ports.pullRequestQueryPort); + if (isExplicitImplement) { + const requestText = explicitCommand.command.arguments.join(' ').trim(); + results.push(new Result({ + id: TASK_ID, + success: true, + executed: true, + steps: ['Explicit implement command selected the authorized repository-change route.'], + payload: { + isFixRequest: false, + isDoRequest: true, + isReviewRequest: false, + targetFindingIds: [], + requestText, + context, + branchOverride, + } as BugbotFixIntent & { context?: typeof context; branchOverride?: string }, + })); + return results; + } if (explicitCommand.kind === 'command' && explicitCommand.command.name === 'fix') { + if (unresolvedIds.size === 0) { + logInfo("No unresolved bugbot findings for explicit fix command; skipping autofix."); + return results; + } const requestedIds = explicitCommand.command.arguments.includes('all') ? [...unresolvedIds] : explicitCommand.command.arguments.filter(id => unresolvedIds.has(id)); @@ -117,7 +137,12 @@ export async function runDetectBugbotFixIntentWorkflow( success: true, executed: true, steps: ["Bugbot fix intent: no response; skipping autofix."], - payload: { isFixRequest: false, isDoRequest: false, targetFindingIds: [] as string[] }, + payload: { + isFixRequest: false, + isDoRequest: false, + isReviewRequest: false, + targetFindingIds: [] as string[], + }, }), ); return results; diff --git a/src/application/usecases/steps/commit/bugbot/schema.ts b/src/application/usecases/steps/commit/bugbot/schema.ts index c0bf7ccc..45da2389 100644 --- a/src/application/usecases/steps/commit/bugbot/schema.ts +++ b/src/application/usecases/steps/commit/bugbot/schema.ts @@ -55,9 +55,9 @@ export const BUGBOT_RESPONSE_SCHEMA = { } as const; /** - * Findings-agent response schema for bugbot fix intent. + * Findings-agent response schema for comment intent. * Given the user comment and the list of unresolved findings, the agent decides whether - * the user is asking to fix one or more of them and which finding ids to target. + * the user is asking to fix findings, apply a general change, or run a read-only review. */ export const BUGBOT_FIX_INTENT_RESPONSE_SCHEMA = { type: 'object', @@ -78,7 +78,12 @@ export const BUGBOT_FIX_INTENT_RESPONSE_SCHEMA = { description: 'True if the user is asking to perform some change or task in the repository (e.g. "add a test for X", "refactor this", "implement feature Y"). False for pure questions or when the only intent is to fix the reported findings (use is_fix_request for that).', }, + is_review_request: { + type: 'boolean', + description: + 'True if the user is asking for a read-only analysis or review of the current issue, branch, or pull request (e.g. "analyze the changes for security issues", "review this PR for bugs"). False for pure questions or file-changing requests.', + }, }, - required: ['is_fix_request', 'target_finding_ids', 'is_do_request'], + required: ['is_fix_request', 'target_finding_ids', 'is_do_request', 'is_review_request'], additionalProperties: false, } as const; diff --git a/src/application/usecases/steps/common/__tests__/think_input_policy.test.ts b/src/application/usecases/steps/common/__tests__/think_input_policy.test.ts index 45c924c2..3e0e8514 100644 --- a/src/application/usecases/steps/common/__tests__/think_input_policy.test.ts +++ b/src/application/usecases/steps/common/__tests__/think_input_policy.test.ts @@ -1,4 +1,4 @@ -import { extractMentionQuestion, getThinkCommentBody } from '../think_input_policy'; +import { containsBotMention, extractMentionQuestion, getThinkCommentBody } from '../think_input_policy'; describe('think input policy', () => { it('selects issue comments before pull request review comments', () => { @@ -10,7 +10,21 @@ describe('think input policy', () => { })).toBe('@bot issue'); }); + it('returns an empty body for an unrelated event', () => { + expect(getThinkCommentBody({ + isIssueComment: false, + isPullRequestReviewComment: false, + })).toBe(''); + }); + it('removes all case-insensitive mentions and preserves escaped usernames', () => { expect(extractMentionQuestion('@a.b explain @A.B please', 'a.b')).toBe('explain please'); }); + + it('matches bot mentions case-insensitively and avoids larger usernames', () => { + expect(containsBotMention('Can @VYPBOT review this?', 'vypbot')).toBe(true); + expect(containsBotMention('Can @vypbot-extra review this?', 'vypbot')).toBe(false); + expect(containsBotMention('Can @vypbot review this?', '@vypbot')).toBe(true); + expect(containsBotMention('Can @vypbot review this?', '')).toBe(false); + }); }); diff --git a/src/application/usecases/steps/common/think_input_policy.ts b/src/application/usecases/steps/common/think_input_policy.ts index 2ddb8319..41d515f5 100644 --- a/src/application/usecases/steps/common/think_input_policy.ts +++ b/src/application/usecases/steps/common/think_input_policy.ts @@ -15,3 +15,11 @@ export function extractMentionQuestion(commentBody: string, tokenUser: string): const escapedUsername = tokenUser.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return commentBody.replace(new RegExp(`@${escapedUsername}`, 'gi'), '').trim(); } + +/** Matches GitHub usernames case-insensitively without matching a larger username. */ +export function containsBotMention(commentBody: string, tokenUser: string): boolean { + const normalizedUser = tokenUser.trim().replace(/^@/u, ''); + if (!normalizedUser) return false; + const escapedUsername = normalizedUser.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`(^|[^A-Za-z0-9_-])@${escapedUsername}(?=$|[^A-Za-z0-9_-])`, 'iu').test(commentBody); +} diff --git a/src/application/usecases/steps/common/think_request_policy.ts b/src/application/usecases/steps/common/think_request_policy.ts index 5c57cfb1..b43297da 100644 --- a/src/application/usecases/steps/common/think_request_policy.ts +++ b/src/application/usecases/steps/common/think_request_policy.ts @@ -1,6 +1,6 @@ import type { Execution } from '../../../../data/model/execution'; import { parseCopilotCommand, type ParsedCopilotCommand } from '../../../../domain/copilot_command'; -import { extractMentionQuestion, getThinkCommentBody } from './think_input_policy'; +import { containsBotMention, extractMentionQuestion, getThinkCommentBody } from './think_input_policy'; import { sanitizeUserCommentForPrompt } from '../commit/bugbot/sanitize_user_comment_for_prompt'; export type ThinkRequestDecision = @@ -30,7 +30,7 @@ export function resolveThinkRequest( if (command.kind === 'invalid') return { kind: 'skip', reason: 'invalid-command', detail: command.reason }; if (command.kind === 'none') { if (!param.tokenUser?.trim()) return { kind: 'skip', reason: 'missing-token' }; - if (!commentBody.includes(`@${param.tokenUser}`)) return { kind: 'skip', reason: 'not-mentioned' }; + if (!containsBotMention(commentBody, param.tokenUser)) return { kind: 'skip', reason: 'not-mentioned' }; } const question = command.kind === 'command' diff --git a/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts b/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts index 606f856c..7555119e 100644 --- a/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts +++ b/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts @@ -146,6 +146,23 @@ describe('AnswerIssueHelpUseCase', () => { expect(results[0].executed).toBe(true); }); + it('includes the bot welcome in the first answer for a newly opened issue', async () => { + mockAskAgent.mockResolvedValue({ answer: 'Here is some help.' }); + mockAddComment.mockResolvedValue(undefined); + const param = baseParam({ + tokenUser: 'vypbot', + eventName: 'issues', + inputs: { eventName: 'issues', action: 'opened' }, + }); + + await useCase.invoke(param); + + const publishedComment = mockAddComment.mock.calls[0][3] as string; + expect(publishedComment).toContain(''); + expect(publishedComment).toContain('Hi! I’m **@vypbot**'); + expect(publishedComment).toContain('Here is some help.'); + }); + it('returns failure when OpenCode returns no answer', async () => { mockAskAgent.mockResolvedValue(undefined); const param = baseParam(); diff --git a/src/application/usecases/steps/issue/answer_issue_help_workflow.ts b/src/application/usecases/steps/issue/answer_issue_help_workflow.ts index f91f79a2..3151d4c1 100644 --- a/src/application/usecases/steps/issue/answer_issue_help_workflow.ts +++ b/src/application/usecases/steps/issue/answer_issue_help_workflow.ts @@ -12,6 +12,7 @@ import { PROJECT_CONTEXT_INSTRUCTION } from '../../../../utils/project_context_i import { getTaskEmoji } from '../../../../utils/task_emoji'; import { extractStructuredAnswer } from '../common/agent_answer_policy'; import { sanitizeAgentMarkdown } from '../../../../application/policies/github_comment_publication_policy'; +import { buildCopilotWelcomeMessage } from '../../../../application/policies/copilot_interaction_policy'; export interface AnswerIssueHelpWorkflowDependencies { issueNotificationPort: IssueNotificationPort; @@ -55,15 +56,24 @@ export async function runAnswerIssueHelpWorkflow( return [noAnswerResult()]; } + const publishedAnswer = isNewIssue(param) + ? `${buildCopilotWelcomeMessage(param.tokenUser)}\n\n${answer}` + : answer; + await dependencies.issueNotificationPort.addComment( param.owner, param.repo, issueNumber, - answer, + publishedAnswer, param.tokens.token, ); logInfo(`Initial help reply posted to issue #${issueNumber}.`); - return [new Result({ id: TASK_ID, success: true, executed: true })]; + return [new Result({ + id: TASK_ID, + success: true, + executed: true, + payload: { welcomePublished: isNewIssue(param) }, + })]; } catch (error) { logError(`Error in ${TASK_ID}: ${error}`); return [new Result({ @@ -75,6 +85,10 @@ export async function runAnswerIssueHelpWorkflow( } } +function isNewIssue(param: Execution): boolean { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} + interface HelpRequest { issueNumber: number; description: string; diff --git a/src/data/repository/__tests__/actor_modification_policy.test.ts b/src/data/repository/__tests__/actor_modification_policy.test.ts index 0864fd03..dcd6c0a4 100644 --- a/src/data/repository/__tests__/actor_modification_policy.test.ts +++ b/src/data/repository/__tests__/actor_modification_policy.test.ts @@ -7,8 +7,12 @@ describe('authorizationForFileModification', () => { }); }); - it('allows only the owner for user-owned repositories', () => { - expect(authorizationForFileModification('alice', 'alice', 'User')).toEqual({ kind: 'owner', allowed: true }); - expect(authorizationForFileModification('alice', 'bob', 'User')).toEqual({ kind: 'owner', allowed: false }); + it('identifies the owner and collaborators for user-owned repositories', () => { + expect(authorizationForFileModification('alice', 'alice', 'User')).toEqual({ + kind: 'user-repository-collaborator', owner: 'alice', actor: 'alice', ownerMatches: true, + }); + expect(authorizationForFileModification('alice', 'bob', 'User')).toEqual({ + kind: 'user-repository-collaborator', owner: 'alice', actor: 'bob', ownerMatches: false, + }); }); }); diff --git a/src/data/repository/actor_modification_policy.ts b/src/data/repository/actor_modification_policy.ts index 7f2e303b..d05f1798 100644 --- a/src/data/repository/actor_modification_policy.ts +++ b/src/data/repository/actor_modification_policy.ts @@ -1,6 +1,8 @@ +import { githubUsersMatch } from '../../domain/github_user_policy'; + export type ModificationAuthorization = - | { kind: 'owner'; allowed: boolean } - | { kind: 'organization-membership'; organization: string; actor: string }; + | { kind: 'organization-membership'; organization: string; actor: string } + | { kind: 'user-repository-collaborator'; owner: string; actor: string; ownerMatches: boolean }; export function authorizationForFileModification( owner: string, @@ -10,5 +12,10 @@ export function authorizationForFileModification( if (ownerType === 'Organization') { return { kind: 'organization-membership', organization: owner, actor }; } - return { kind: 'owner', allowed: actor === owner }; + return { + kind: 'user-repository-collaborator', + owner, + actor, + ownerMatches: githubUsersMatch(actor, owner), + }; } diff --git a/src/data/repository/organization/__tests__/actor_authorization_repository.test.ts b/src/data/repository/organization/__tests__/actor_authorization_repository.test.ts index 20fb2308..ec9ad011 100644 --- a/src/data/repository/organization/__tests__/actor_authorization_repository.test.ts +++ b/src/data/repository/organization/__tests__/actor_authorization_repository.test.ts @@ -7,11 +7,13 @@ jest.mock('../../../../utils/logger', () => ({ describe('ActorAuthorizationRepository', () => { const getByUsername = jest.fn(); const checkMembershipForUser = jest.fn(); + const getCollaboratorPermissionLevel = jest.fn(); const repository = new ActorAuthorizationRepository({ getClient: jest.fn(() => ({ rest: { users: { getByUsername }, orgs: { checkMembershipForUser }, + repos: { getCollaboratorPermissionLevel }, }, })), } as any); @@ -20,37 +22,74 @@ describe('ActorAuthorizationRepository', () => { jest.clearAllMocks(); getByUsername.mockResolvedValue({ data: { type: 'Organization' } }); checkMembershipForUser.mockResolvedValue({}); + getCollaboratorPermissionLevel.mockResolvedValue({ data: { permission: 'pull' } }); }); it('allows an organization actor when membership succeeds', async () => { - await expect(repository.isActorAllowedToModifyFiles('acme', 'alice', 'token')).resolves.toBe(true); + await expect(repository.isActorAllowedToModifyFiles('acme', 'project', 'alice', 'token')).resolves.toBe(true); expect(checkMembershipForUser).toHaveBeenCalledWith({ org: 'acme', username: 'alice' }); }); it('denies an organization actor when membership returns not found', async () => { checkMembershipForUser.mockRejectedValue({ status: 404 }); - await expect(repository.isActorAllowedToModifyFiles('acme', 'alice', 'token')).resolves.toBe(false); + await expect(repository.isActorAllowedToModifyFiles('acme', 'project', 'alice', 'token')).resolves.toBe(false); }); it('denies and logs unexpected membership failures', async () => { checkMembershipForUser.mockRejectedValue(new Error('membership unavailable')); - await expect(repository.isActorAllowedToModifyFiles('acme', 'alice', 'token')).resolves.toBe(false); + await expect(repository.isActorAllowedToModifyFiles('acme', 'project', 'alice', 'token')).resolves.toBe(false); + }); + + it('denies and logs a non-Error membership failure', async () => { + checkMembershipForUser.mockRejectedValue({ status: 500, message: 'membership unavailable' }); + await expect(repository.isActorAllowedToModifyFiles('acme', 'project', 'alice', 'token')).resolves.toBe(false); }); it('allows the owner of a user repository without membership lookup', async () => { getByUsername.mockResolvedValue({ data: { type: 'User' } }); - await expect(repository.isActorAllowedToModifyFiles('alice', 'alice', 'token')).resolves.toBe(true); + await expect(repository.isActorAllowedToModifyFiles('alice', 'project', 'alice', 'token')).resolves.toBe(true); expect(checkMembershipForUser).not.toHaveBeenCalled(); + expect(getCollaboratorPermissionLevel).not.toHaveBeenCalled(); }); - it('denies a different actor on a user repository', async () => { + it('allows a write collaborator on a user repository', async () => { getByUsername.mockResolvedValue({ data: { type: 'User' } }); - await expect(repository.isActorAllowedToModifyFiles('alice', 'bob', 'token')).resolves.toBe(false); + getCollaboratorPermissionLevel.mockResolvedValue({ data: { permission: 'push' } }); + await expect(repository.isActorAllowedToModifyFiles('alice', 'project', 'bob', 'token')).resolves.toBe(true); expect(checkMembershipForUser).not.toHaveBeenCalled(); + expect(getCollaboratorPermissionLevel).toHaveBeenCalledWith({ owner: 'alice', repo: 'project', username: 'bob' }); + }); + + it('denies a read-only collaborator on a user repository', async () => { + getByUsername.mockResolvedValue({ data: { type: 'User' } }); + getCollaboratorPermissionLevel.mockResolvedValue({ data: { permission: 'pull' } }); + await expect(repository.isActorAllowedToModifyFiles('alice', 'project', 'bob', 'token')).resolves.toBe(false); + expect(checkMembershipForUser).not.toHaveBeenCalled(); + }); + + it('denies and logs an unexpected collaborator permission failure', async () => { + getByUsername.mockResolvedValue({ data: { type: 'User' } }); + getCollaboratorPermissionLevel.mockRejectedValue(new Error('permission service unavailable')); + + await expect(repository.isActorAllowedToModifyFiles('alice', 'project', 'bob', 'token')).resolves.toBe(false); + }); + + it('denies a collaborator when GitHub returns no permission value', async () => { + getByUsername.mockResolvedValue({ data: { type: 'User' } }); + getCollaboratorPermissionLevel.mockResolvedValue({ data: {} }); + + await expect(repository.isActorAllowedToModifyFiles('alice', 'project', 'bob', 'token')).resolves.toBe(false); + }); + + it('does not log a missing collaborator as an unexpected permission failure', async () => { + getByUsername.mockResolvedValue({ data: { type: 'User' } }); + getCollaboratorPermissionLevel.mockRejectedValue({ status: 404 }); + + await expect(repository.isActorAllowedToModifyFiles('alice', 'project', 'bob', 'token')).resolves.toBe(false); }); it('denies when owner lookup fails', async () => { getByUsername.mockRejectedValue(new Error('lookup unavailable')); - await expect(repository.isActorAllowedToModifyFiles('acme', 'alice', 'token')).resolves.toBe(false); + await expect(repository.isActorAllowedToModifyFiles('acme', 'project', 'alice', 'token')).resolves.toBe(false); }); }); diff --git a/src/data/repository/organization/actor_authorization_repository.ts b/src/data/repository/organization/actor_authorization_repository.ts index e7f92f45..4054d7c5 100644 --- a/src/data/repository/organization/actor_authorization_repository.ts +++ b/src/data/repository/organization/actor_authorization_repository.ts @@ -6,15 +6,18 @@ import type { GithubActorAuthorizationClient } from "../../../infrastructure/git export class ActorAuthorizationRepository implements ActorAuthorizationPort { constructor(private readonly githubClient: GithubClientPort) {} - isActorAllowedToModifyFiles = async (owner: string, actor: string, token: string): Promise => { + isActorAllowedToModifyFiles = async (owner: string, repo: string, actor: string, token: string): Promise => { try { const octokit = this.githubClient.getClient(token); const { data: ownerUser } = await octokit.rest.users.getByUsername({ username: owner }); const authorization = authorizationForFileModification(owner, actor, ownerUser.type); - if (authorization.kind === 'owner') return authorization.allowed; - return this.checkOrganizationMembership(octokit, authorization.organization, authorization.actor, owner, actor); + if (authorization.kind === 'organization-membership') { + return this.checkOrganizationMembership(octokit, authorization.organization, authorization.actor, owner, actor); + } + if (authorization.ownerMatches) return true; + return this.checkUserRepositoryPermission(octokit, owner, actor, repo); } catch (err) { - logDebugInfo(`isActorAllowedToModifyFiles(${owner}, ${actor}): ${err instanceof Error ? err.message : String(err)}`); + logDebugInfo(`isActorAllowedToModifyFiles(${owner}, ${repo}, ${actor}): ${err instanceof Error ? err.message : String(err)}`); return false; } }; @@ -30,10 +33,38 @@ export class ActorAuthorizationRepository implements ActorAuthorizationPort { await octokit.rest.orgs.checkMembershipForUser({ org: organization, username: actor }); return true; } catch (membershipErr: unknown) { - const status = (membershipErr as { status?: number })?.status; - if (status === 404) return false; - logDebugInfo(`checkMembershipForUser(${owner}, ${originalActor}): ${membershipErr instanceof Error ? membershipErr.message : String(membershipErr)}`); + logUnlessNotFound( + membershipErr, + `checkMembershipForUser(${owner}, ${originalActor})`, + ); return false; } } + + private async checkUserRepositoryPermission( + octokit: GithubActorAuthorizationClient, + owner: string, + actor: string, + repo: string, + ): Promise { + try { + const response = await octokit.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username: actor, + }); + return ['admin', 'maintain', 'push'].includes(response.data.permission ?? ''); + } catch (permissionErr: unknown) { + logUnlessNotFound( + permissionErr, + `getCollaboratorPermissionLevel(${owner}, ${repo}, ${actor})`, + ); + return false; + } + } +} + +function logUnlessNotFound(error: unknown, operation: string): void { + if ((error as { status?: number })?.status === 404) return; + logDebugInfo(`${operation}: ${error instanceof Error ? error.message : String(error)}`); } diff --git a/src/domain/__tests__/copilot_command.test.ts b/src/domain/__tests__/copilot_command.test.ts index e0d78dca..2371c71d 100644 --- a/src/domain/__tests__/copilot_command.test.ts +++ b/src/domain/__tests__/copilot_command.test.ts @@ -3,6 +3,11 @@ import { parseCopilotCommand } from '../copilot_command'; describe('Copilot command policy', () => { it.each([ ['/copilot plan', 'plan'], + ['/copilot help', 'help'], + ['/copilot analyze', 'analyze'], + ['/copilot explain src/auth/login.ts', 'explain'], + ['/copilot diagnose', 'diagnose'], + ['/copilot implement add a regression test', 'implement'], ['/copilot review security regression', 'review'], ['/copilot status', 'status'], ['/copilot description', 'description'], @@ -17,9 +22,10 @@ describe('Copilot command policy', () => { expect(parseCopilotCommand('Please /copilot plan this')).toEqual({ kind: 'none' }); }); - it('rejects unknown commands and missing finding ids', () => { + it('rejects unknown commands and missing required arguments', () => { expect(parseCopilotCommand('/copilot deploy')).toMatchObject({ kind: 'invalid' }); expect(parseCopilotCommand('/copilot fix')).toMatchObject({ kind: 'invalid' }); + expect(parseCopilotCommand('/copilot implement')).toMatchObject({ kind: 'invalid' }); }); diff --git a/src/domain/copilot_command.ts b/src/domain/copilot_command.ts index d328445e..fe038680 100644 --- a/src/domain/copilot_command.ts +++ b/src/domain/copilot_command.ts @@ -1,16 +1,21 @@ /** Explicit commands are the safe, deterministic entry point for mutations. */ export const COPILOT_COMMAND_NAMES = [ + 'help', + 'analyze', 'plan', 'clarify', 'estimate', 'test-plan', 'status', 'description', + 'explain', + 'diagnose', 'review', 'findings', 'fix', 'dismiss', 'recheck', + 'implement', ] as const; export type CopilotCommandName = typeof COPILOT_COMMAND_NAMES[number]; @@ -51,8 +56,8 @@ export function parseCopilotCommand(raw: unknown): CopilotCommandParseResult { if (tokens.length > MAX_ARGUMENTS) { return { kind: 'invalid', reason: `Copilot commands accept at most ${MAX_ARGUMENTS} arguments.` }; } - if ((name === 'fix' || name === 'dismiss') && tokens.length === 0) { - return { kind: 'invalid', reason: `/${name} requires at least one finding id.` }; + if ((name === 'fix' || name === 'dismiss' || name === 'implement') && tokens.length === 0) { + return { kind: 'invalid', reason: `/${name} requires at least one argument.` }; } return { kind: 'command', diff --git a/src/infrastructure/github/ports/github_identity_provider_ports.ts b/src/infrastructure/github/ports/github_identity_provider_ports.ts index 11bebcc3..48dac363 100644 --- a/src/infrastructure/github/ports/github_identity_provider_ports.ts +++ b/src/infrastructure/github/ports/github_identity_provider_ports.ts @@ -24,6 +24,13 @@ export interface GithubActorAuthorizationClient { orgs: { checkMembershipForUser(parameters: { org: string; username: string }): Promise; }; + repos: { + getCollaboratorPermissionLevel(parameters: { + owner: string; + repo: string; + username: string; + }): Promise<{ data: { permission?: string } }>; + }; }; } diff --git a/src/prompts/__tests__/bugbot_fix_intent.test.ts b/src/prompts/__tests__/bugbot_fix_intent.test.ts index 8233305f..33a40d4d 100644 --- a/src/prompts/__tests__/bugbot_fix_intent.test.ts +++ b/src/prompts/__tests__/bugbot_fix_intent.test.ts @@ -17,6 +17,7 @@ describe('getBugbotFixIntentPrompt', () => { expect(prompt).toContain('is_fix_request'); expect(prompt).toContain('target_finding_ids'); expect(prompt).toContain('is_do_request'); + expect(prompt).toContain('is_review_request'); expect(prompt).not.toContain('{{'); }); @@ -29,6 +30,7 @@ describe('getBugbotFixIntentPrompt', () => { }); expect(prompt).toContain('(No unresolved findings.)'); expect(prompt).toContain('fix all'); + expect(prompt).toContain('is_review_request'); expect(prompt).not.toContain('{{'); }); }); diff --git a/src/prompts/bugbot_fix_intent.ts b/src/prompts/bugbot_fix_intent.ts index cead3cd7..09b7bda4 100644 --- a/src/prompts/bugbot_fix_intent.ts +++ b/src/prompts/bugbot_fix_intent.ts @@ -1,9 +1,9 @@ /** - * Prompt for detecting if user comment is a fix request and which finding ids to target. + * Prompt for detecting the action requested by a user comment. */ import { fillTemplate } from './fill'; -const TEMPLATE = `You are analyzing a user comment on an issue or pull request to decide whether they are asking to fix one or more reported code findings (bugs, vulnerabilities, or quality issues). +const TEMPLATE = `You are analyzing a user comment on an issue or pull request to classify the requested Copilot action. The available actions are: fix reported findings, apply a general repository change, run a read-only code review, or answer a question. {{projectContextInstruction}} @@ -17,8 +17,9 @@ const TEMPLATE = `You are analyzing a user comment on an issue or pull request t 1. Is this comment clearly a request to fix one or more of the findings above? (e.g. "fix it", "arreglalo", "fix this", "fix all", "fix vulnerability X", "corrige", "fix the bug in src/foo.ts"). If the user is asking a question, discussing something else, or the intent is ambiguous, set \`is_fix_request\` to false. 2. If it is a fix request, which finding ids should be fixed? Return their exact ids in \`target_finding_ids\`. If the user says "fix all" or equivalent, include every id from the list above. If they refer to a specific finding (e.g. by replying to a comment that contains one finding), return only that finding's id. Use only ids that appear in the list above. 3. Is the user asking to perform some other change or task in the repo? (e.g. "add a test for X", "refactor this", "implement feature Y", "haz que Z"). If yes, set \`is_do_request\` to true. Set false for pure questions or when the only intent is to fix the listed findings. +4. Is the user asking for a read-only review or analysis of the current code? (e.g. "analyze the changes for security issues", "review this PR for bugs", "look for performance problems"). If yes, set \`is_review_request\` to true. Do not set it for a question about how the code works or for a request that changes files. -Respond with a JSON object: \`is_fix_request\` (boolean), \`target_finding_ids\` (array of strings; empty when \`is_fix_request\` is false), and \`is_do_request\` (boolean).`; +Respond with a JSON object: \`is_fix_request\` (boolean), \`target_finding_ids\` (array of strings; empty when \`is_fix_request\` is false), \`is_do_request\` (boolean), and \`is_review_request\` (boolean).`; export type BugbotFixIntentParams = { projectContextInstruction: string; From 1e63605dce66330dbd3dfe48062fa4b655460380 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 4 Sep 2026 11:20:53 +0200 Subject: [PATCH 07/11] develop: refresh generated build artifacts --- build/cli/index.js | 362 ++++++++++++++---- .../policies/copilot_interaction_policy.d.ts | 11 + .../ports/actor_authorization_ports.d.ts | 2 +- .../comment_automation_action_workflow.d.ts | 2 +- .../comment_automation_route_policy.d.ts | 4 +- .../detect_bugbot_fix_intent_policy.d.ts | 2 + .../detect_bugbot_fix_intent_workflow.d.ts | 2 +- .../usecases/steps/commit/bugbot/schema.d.ts | 10 +- .../steps/common/think_input_policy.d.ts | 2 + .../repository/actor_modification_policy.d.ts | 8 +- .../actor_authorization_repository.d.ts | 3 +- build/cli/src/domain/copilot_command.d.ts | 2 +- .../ports/github_identity_provider_ports.d.ts | 11 + build/github_action/index.js | 362 ++++++++++++++---- .../policies/copilot_interaction_policy.d.ts | 11 + .../ports/actor_authorization_ports.d.ts | 2 +- .../comment_automation_action_workflow.d.ts | 2 +- .../comment_automation_route_policy.d.ts | 4 +- .../detect_bugbot_fix_intent_policy.d.ts | 2 + .../detect_bugbot_fix_intent_workflow.d.ts | 2 +- .../usecases/steps/commit/bugbot/schema.d.ts | 10 +- .../steps/common/think_input_policy.d.ts | 2 + .../repository/actor_modification_policy.d.ts | 8 +- .../actor_authorization_repository.d.ts | 3 +- .../src/domain/copilot_command.d.ts | 2 +- .../ports/github_identity_provider_ports.d.ts | 11 + 26 files changed, 680 insertions(+), 162 deletions(-) create mode 100644 build/cli/src/application/policies/copilot_interaction_policy.d.ts create mode 100644 build/github_action/src/application/policies/copilot_interaction_policy.d.ts diff --git a/build/cli/index.js b/build/cli/index.js index b582751a..8d5b4a45 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -56283,6 +56283,85 @@ function composeTranslatedComment(translatedValue, originalComment) { } +/***/ }), + +/***/ 90108: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.COPILOT_WELCOME_MARKER = exports.DEFAULT_COPILOT_BOT_USERNAME = void 0; +exports.normalizeCopilotBotUsername = normalizeCopilotBotUsername; +exports.buildCopilotHelpMessage = buildCopilotHelpMessage; +exports.buildCopilotWelcomeMessage = buildCopilotWelcomeMessage; +exports.buildCopilotWelcomeResult = buildCopilotWelcomeResult; +const result_1 = __nccwpck_require__(73817); +exports.DEFAULT_COPILOT_BOT_USERNAME = 'vypbot'; +exports.COPILOT_WELCOME_MARKER = ''; +const SAFE_GITHUB_USERNAME = /^[A-Za-z0-9-]+$/u; +/** Keeps the bot identity safe when it is rendered into a GitHub comment. */ +function normalizeCopilotBotUsername(username) { + const candidate = username?.trim().replace(/^@/u, ''); + return candidate && SAFE_GITHUB_USERNAME.test(candidate) + ? candidate + : exports.DEFAULT_COPILOT_BOT_USERNAME; +} +/** Renders the stable command reference used by /copilot help. */ +function buildCopilotHelpMessage(username) { + const bot = normalizeCopilotBotUsername(username); + return `## Copilot commands + +I’m **@${bot}**, the repository assistant. Use these commands on an issue or pull request: + +### Read-only + +- \`/copilot help\` — show this command reference. +- \`/copilot plan\` — propose an implementation plan. +- \`/copilot clarify\` — identify missing information and assumptions. +- \`/copilot estimate\` — estimate scope and complexity. +- \`/copilot test-plan\` — propose a focused testing strategy. +- \`/copilot explain \` — explain code or behavior. +- \`/copilot diagnose\` — investigate a reported problem and suggest likely causes. +- \`/copilot analyze\` — review the current issue, branch, or pull request for potential problems. +- \`/copilot review\` — run the Bugbot review. +- \`/copilot findings\` — show potential findings from the current code. +- \`/copilot recheck\` — re-run the review and reconcile findings. +- \`/copilot description\` — refresh the pull-request description. +- \`/copilot status\` — show the current automation status. + +### Changes + +- \`/copilot fix \` — fix one reported finding. +- \`/copilot fix all\` — fix all unresolved findings. +- \`/copilot dismiss \` — dismiss a finding. +- \`/copilot implement \` — apply an explicitly requested repository change. + +You can also ask a question in natural language by mentioning **@${bot}**. File-changing commands are restricted to authorized maintainers, run the configured checks, and report the resulting changes.`; +} +/** Renders the one-time onboarding comment for a newly created issue. */ +function buildCopilotWelcomeMessage(username) { + const bot = normalizeCopilotBotUsername(username); + return `${exports.COPILOT_WELCOME_MARKER} + +Hi! I’m **@${bot}**, the Copilot assistant for this repository. + +I can answer questions, explain the codebase, propose implementation and test plans, review issues and pull requests for potential bugs or security problems, and help authorized maintainers apply changes. + +Try \`/copilot help\` to see the available commands, or mention **@${bot}** with your question.`; +} +/** Creates a publishable result for issues that have no agent-generated reply. */ +function buildCopilotWelcomeResult(username) { + return new result_1.Result({ + id: 'CopilotWelcomeUseCase', + success: true, + executed: true, + stepFormat: 'markdown', + steps: [buildCopilotWelcomeMessage(username)], + }); +} + + /***/ }), /***/ 8428: @@ -58351,6 +58430,7 @@ exports.buildRecommendationResult = buildRecommendationResult; const result_1 = __nccwpck_require__(73817); const recommendation_policy_1 = __nccwpck_require__(39410); const logging_ports_1 = __nccwpck_require__(6152); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); function buildRecommendationResult(param, taskId, response, issueDescriptionFingerprint, previousRecommendation, issueNumber) { const steps = extractRecommendationText(response); if (!steps) { @@ -58369,15 +58449,21 @@ function buildRecommendationResult(param, taskId, response, issueDescriptionFing recommendationFingerprint, recommendation: (0, recommendation_policy_1.limitStoredRecommendation)(steps), }; + const stepsWithWelcome = isNewIssue(param) + ? [(0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(param.tokenUser), '## Recommended implementation steps', steps] + : ['## Recommended implementation steps', steps]; return [new result_1.Result({ id: taskId, success: true, executed: true, stepFormat: 'markdown', - steps: ['## Recommended implementation steps', steps], + steps: stepsWithWelcome, payload: { issueNumber, recommendedSteps: steps, recommendationState }, })]; } +function isNewIssue(param) { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} function skipUnchangedRecommendation(param, previous, fingerprint, reason) { param.currentConfiguration.recommendationState = { ...previous, issueDescriptionFingerprint: fingerprint }; (0, logging_ports_1.logInfo)(`RecommendSteps: ${reason}; skipping recommendation comment.`); @@ -58647,40 +58733,62 @@ const commit_user_request_workflow_1 = __nccwpck_require__(43393); const logging_ports_1 = __nccwpck_require__(6152); /** Runs the selected mutating action and returns any result records it produces. */ async function runCommentAutomationAction(param, options, route, intentPayload, ports) { - if (route === "autofix" && intentPayload) { - (0, logging_ports_1.logInfo)("Running bugbot autofix."); - const autofixResults = await options.autofixUseCase.invoke({ - execution: param, - targetFindingIds: intentPayload.targetFindingIds, - userComment: options.userComment, - context: intentPayload.context, - branchOverride: intentPayload.branchOverride, - }); - const resolutionErrors = await (0, commit_autofix_and_resolve_workflow_1.commitAutofixAndResolveFindings)(param, intentPayload, autofixResults, ports.authenticatedUserPort, ports.bugbotResolutionPorts, ports.gitCommitPort); - if (resolutionErrors.length > 0) { - autofixResults.push(new result_1.Result({ - id: `${options.taskId}.AutofixPostflight`, + if (route === "review") + return runReviewAction(param, options); + if (route === "autofix") + return runAutofixAction(param, options, intentPayload, ports); + if (route === "do-user-request") + return runDoUserRequestAction(param, options, intentPayload, ports); + return []; +} +async function runReviewAction(param, options) { + if (!options.reviewPotentialProblemsUseCase) { + return [new result_1.Result({ + id: `${options.taskId}.Review`, success: false, - executed: true, - steps: [ - "Autofix postflight failed: commit/push or finding reconciliation did not complete.", - ], - errors: resolutionErrors, - })); - } - return autofixResults; + executed: false, + errors: ["Read-only review is not available in this composition."], + })]; } - if (route === "do-user-request" && intentPayload) { - (0, logging_ports_1.logInfo)("Running do user request."); - const doResults = await options.doUserRequestUseCase.invoke({ - execution: param, - userComment: options.userComment, - branchOverride: intentPayload.branchOverride, - }); - const commitResults = await (0, commit_user_request_workflow_1.commitUserRequestIfSuccessful)(param, intentPayload.branchOverride, doResults, ports.authenticatedUserPort, ports.gitCommitPort); - return [...doResults, ...commitResults]; + (0, logging_ports_1.logInfo)("Running natural-language read-only review."); + return options.reviewPotentialProblemsUseCase.invoke(param); +} +async function runAutofixAction(param, options, intentPayload, ports) { + if (!intentPayload) + return []; + (0, logging_ports_1.logInfo)("Running bugbot autofix."); + const autofixResults = await options.autofixUseCase.invoke({ + execution: param, + targetFindingIds: intentPayload.targetFindingIds, + userComment: options.userComment, + context: intentPayload.context, + branchOverride: intentPayload.branchOverride, + }); + const resolutionErrors = await (0, commit_autofix_and_resolve_workflow_1.commitAutofixAndResolveFindings)(param, intentPayload, autofixResults, ports.authenticatedUserPort, ports.bugbotResolutionPorts, ports.gitCommitPort); + if (resolutionErrors.length > 0) { + autofixResults.push(new result_1.Result({ + id: `${options.taskId}.AutofixPostflight`, + success: false, + executed: true, + steps: [ + "Autofix postflight failed: commit/push or finding reconciliation did not complete.", + ], + errors: resolutionErrors, + })); } - return []; + return autofixResults; +} +async function runDoUserRequestAction(param, options, intentPayload, ports) { + if (!intentPayload) + return []; + (0, logging_ports_1.logInfo)("Running do user request."); + const doResults = await options.doUserRequestUseCase.invoke({ + execution: param, + userComment: intentPayload.requestText?.trim() || options.userComment, + branchOverride: intentPayload.branchOverride, + }); + const commitResults = await (0, commit_user_request_workflow_1.commitUserRequestIfSuccessful)(param, intentPayload.branchOverride, doResults, ports.authenticatedUserPort, ports.gitCommitPort); + return [...doResults, ...commitResults]; } @@ -58696,20 +58804,32 @@ exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; const result_1 = __nccwpck_require__(73817); const status_command_policy_1 = __nccwpck_require__(3449); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); /** Executes deterministic /copilot commands without routing them through intent detection. */ async function runExplicitCommentCommand(param, options, command, actorAuthorizationPort) { + if (command.name === 'help') + return runHelpCommand(param, options); if (command.name === 'status') return [(0, status_command_policy_1.buildCopilotStatusResult)(param, options.taskId)]; if (command.name === 'dismiss') return runDismissCommand(param, options, command, actorAuthorizationPort); if (command.name === 'description') return runDescriptionCommand(param, options); - if (['review', 'findings', 'recheck'].includes(command.name)) + if (['analyze', 'review', 'findings', 'recheck'].includes(command.name)) return runReviewCommand(param, options, command); - if (command.name === 'fix') + if (command.name === 'fix' || command.name === 'implement') return undefined; return runThinkCommand(param, options, command); } +function runHelpCommand(param, options) { + return [new result_1.Result({ + id: `${options.taskId}.Help`, + success: true, + executed: true, + stepFormat: 'markdown', + steps: [(0, copilot_interaction_policy_1.buildCopilotHelpMessage)(param.tokenUser)], + })]; +} async function runDescriptionCommand(param, options) { if (!options.updatePullRequestDescriptionUseCase) { return [new result_1.Result({ @@ -58722,7 +58842,7 @@ async function runDescriptionCommand(param, options) { return options.updatePullRequestDescriptionUseCase.invokeExplicit(param); } async function runDismissCommand(param, options, command, actorAuthorizationPort) { - const allowed = await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.actor, param.tokens.token); + const allowed = await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.repo, param.actor, param.tokens.token); if (!allowed || !options.dismissBugbotFindingsUseCase) { return [new result_1.Result({ id: options.taskId, @@ -58822,11 +58942,16 @@ exports.resolveCommentAutomationDecision = resolveCommentAutomationDecision; const logging_ports_1 = __nccwpck_require__(6152); const bugbot_fix_intent_payload_1 = __nccwpck_require__(25734); const comment_automation_route_policy_1 = __nccwpck_require__(47058); +const think_input_policy_1 = __nccwpck_require__(59687); +const copilot_command_1 = __nccwpck_require__(11771); async function resolveCommentAutomationDecision(param, options, actorAuthorizationPort) { (0, logging_ports_1.logInfo)("Running bugbot fix intent detection (before Think)."); const intentResults = await options.intentUseCase.invoke(param); const intentPayload = (0, bugbot_fix_intent_payload_1.getBugbotFixIntentPayload)(intentResults); - const route = (0, comment_automation_route_policy_1.resolveCommentAutomationRoute)(intentPayload, await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.actor, param.tokens.token)); + const parsedCommand = (0, copilot_command_1.parseCopilotCommand)(options.userComment); + const explicitMutationCommand = parsedCommand.kind === 'command' + && (parsedCommand.command.name === 'fix' || parsedCommand.command.name === 'implement'); + const route = (0, comment_automation_route_policy_1.resolveCommentAutomationRoute)(intentPayload, await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.repo, param.actor, param.tokens.token), (0, think_input_policy_1.containsBotMention)(options.userComment, param.tokenUser ?? ''), explicitMutationCommand); logIntent(intentPayload); return { intentResults, intentPayload, route }; } @@ -58872,7 +58997,11 @@ async function runNaturalLanguageCommentAutomation(param, options, actorAuthoriz Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveCommentAutomationRoute = resolveCommentAutomationRoute; const bugbot_fix_intent_payload_1 = __nccwpck_require__(25734); -function resolveCommentAutomationRoute(payload, allowedToModifyFiles) { +function resolveCommentAutomationRoute(payload, allowedToModifyFiles, botMentioned = false, explicitMutationCommand = false) { + if (!botMentioned && !explicitMutationCommand) + return 'think'; + if (botMentioned && payload?.isReviewRequest) + return 'review'; if (!allowedToModifyFiles) return 'think'; if ((0, bugbot_fix_intent_payload_1.canRunBugbotAutofix)(payload)) @@ -59373,6 +59502,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runIssueWorkflow = runIssueWorkflow; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); /** Coordinates issue lifecycle steps in their required sequential order. */ async function runIssueWorkflow(param, taskId, ports) { const results = []; @@ -59417,10 +59547,24 @@ async function runIssueWorkflow(param, taskId, ports) { } const recommendation = resolveIssueRecommendation(param, ports); if (recommendation) { - results.push(...(await recommendation.invoke(param))); + const recommendationResults = await recommendation.invoke(param); + results.push(...recommendationResults); + if (isNewIssue(param) && !containsWelcome(recommendationResults)) { + results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(param.tokenUser)); + } + } + else if (isNewIssue(param)) { + results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(param.tokenUser)); } return results; } +function containsWelcome(results) { + return results.some((result) => result.steps.some((step) => step.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER)) + || (0, result_1.getResultPayload)(result.payload)?.welcomePublished === true); +} +function isNewIssue(param) { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} function resolveIssueRecommendation(param, ports) { if (!param.issue.opened && !param.issue.descriptionEdited) return undefined; @@ -60841,13 +60985,14 @@ function parseBugbotFixIntentResponse(response, unresolvedFindingIds) { const payload = response; const isFixRequest = payload.is_fix_request === true; const isDoRequest = payload.is_do_request === true; + const isReviewRequest = payload.is_review_request === true; const requestedIds = Array.isArray(payload.target_finding_ids) ? payload.target_finding_ids.filter((id) => typeof id === "string") : []; const targetFindingIds = isFixRequest ? unique(requestedIds.filter((id) => unresolvedFindingIds.has(id))) : []; - return { isFixRequest, isDoRequest, targetFindingIds }; + return { isFixRequest, isDoRequest, targetFindingIds, isReviewRequest }; } function unique(values) { return [...new Set(values)]; @@ -60906,7 +61051,7 @@ const load_bugbot_context_use_case_1 = __nccwpck_require__(4050); const schema_1 = __nccwpck_require__(16808); const detect_bugbot_fix_intent_policy_1 = __nccwpck_require__(14796); const TASK_ID = "DetectBugbotFixIntentUseCase"; -/** Detects whether a comment targets Bugbot findings and returns the validated intent payload. */ +/** Detects whether a comment requests a finding fix, repository change, or read-only review. */ async function runDetectBugbotFixIntentWorkflow(param, ports) { const results = []; if (param.issueNumber <= 0 && param.pullRequest.number <= 0) { @@ -60920,7 +61065,8 @@ async function runDetectBugbotFixIntentWorkflow(param, ports) { } const explicitCommand = (0, copilot_command_1.parseCopilotCommand)(commentBody); const isExplicitFix = explicitCommand.kind === 'command' && explicitCommand.command.name === 'fix'; - if (!isExplicitFix && !(0, agent_1.isAgentConfigurationReady)(param.ai?.getAgentConfiguration("findings"))) { + const isExplicitImplement = explicitCommand.kind === 'command' && explicitCommand.command.name === 'implement'; + if (!isExplicitFix && !isExplicitImplement && !(0, agent_1.isAgentConfigurationReady)(param.ai?.getAgentConfiguration("findings"))) { (0, logging_ports_1.logInfo)("Agent not configured; skipping bugbot fix intent detection."); return results; } @@ -60937,14 +61083,33 @@ async function runDetectBugbotFixIntentWorkflow(param, ports) { : undefined; const context = await (0, load_bugbot_context_use_case_1.loadBugbotContext)(param, contextOptions, ports.contextPorts); const unresolvedWithBody = context.unresolvedFindingsWithBody ?? []; - if (unresolvedWithBody.length === 0) { - (0, logging_ports_1.logInfo)("No unresolved bugbot findings for this issue/PR; skipping bugbot fix intent detection."); - return results; - } const unresolvedIds = new Set(unresolvedWithBody.map((finding) => finding.id)); const unresolvedFindings = (0, detect_bugbot_fix_intent_policy_1.buildUnresolvedFindingSummaries)(unresolvedWithBody); const parentCommentBody = await resolveParentCommentBody(param, ports.pullRequestQueryPort); + if (isExplicitImplement) { + const requestText = explicitCommand.command.arguments.join(' ').trim(); + results.push(new result_1.Result({ + id: TASK_ID, + success: true, + executed: true, + steps: ['Explicit implement command selected the authorized repository-change route.'], + payload: { + isFixRequest: false, + isDoRequest: true, + isReviewRequest: false, + targetFindingIds: [], + requestText, + context, + branchOverride, + }, + })); + return results; + } if (explicitCommand.kind === 'command' && explicitCommand.command.name === 'fix') { + if (unresolvedIds.size === 0) { + (0, logging_ports_1.logInfo)("No unresolved bugbot findings for explicit fix command; skipping autofix."); + return results; + } const requestedIds = explicitCommand.command.arguments.includes('all') ? [...unresolvedIds] : explicitCommand.command.arguments.filter(id => unresolvedIds.has(id)); @@ -60983,7 +61148,12 @@ async function runDetectBugbotFixIntentWorkflow(param, ports) { success: true, executed: true, steps: ["Bugbot fix intent: no response; skipping autofix."], - payload: { isFixRequest: false, isDoRequest: false, targetFindingIds: [] }, + payload: { + isFixRequest: false, + isDoRequest: false, + isReviewRequest: false, + targetFindingIds: [], + }, })); return results; } @@ -62102,9 +62272,9 @@ exports.BUGBOT_RESPONSE_SCHEMA = { additionalProperties: false, }; /** - * Findings-agent response schema for bugbot fix intent. + * Findings-agent response schema for comment intent. * Given the user comment and the list of unresolved findings, the agent decides whether - * the user is asking to fix one or more of them and which finding ids to target. + * the user is asking to fix findings, apply a general change, or run a read-only review. */ exports.BUGBOT_FIX_INTENT_RESPONSE_SCHEMA = { type: 'object', @@ -62122,8 +62292,12 @@ exports.BUGBOT_FIX_INTENT_RESPONSE_SCHEMA = { type: 'boolean', description: 'True if the user is asking to perform some change or task in the repository (e.g. "add a test for X", "refactor this", "implement feature Y"). False for pure questions or when the only intent is to fix the reported findings (use is_fix_request for that).', }, + is_review_request: { + type: 'boolean', + description: 'True if the user is asking for a read-only analysis or review of the current issue, branch, or pull request (e.g. "analyze the changes for security issues", "review this PR for bugs"). False for pure questions or file-changing requests.', + }, }, - required: ['is_fix_request', 'target_finding_ids', 'is_do_request'], + required: ['is_fix_request', 'target_finding_ids', 'is_do_request', 'is_review_request'], additionalProperties: false, }; @@ -63651,6 +63825,7 @@ async function queryThinkAnswer(param, prompt, repository, agentTask) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getThinkCommentBody = getThinkCommentBody; exports.extractMentionQuestion = extractMentionQuestion; +exports.containsBotMention = containsBotMention; function getThinkCommentBody(source) { if (source.isIssueComment) return source.issueCommentBody ?? ''; @@ -63662,6 +63837,14 @@ function extractMentionQuestion(commentBody, tokenUser) { const escapedUsername = tokenUser.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return commentBody.replace(new RegExp(`@${escapedUsername}`, 'gi'), '').trim(); } +/** Matches GitHub usernames case-insensitively without matching a larger username. */ +function containsBotMention(commentBody, tokenUser) { + const normalizedUser = tokenUser.trim().replace(/^@/u, ''); + if (!normalizedUser) + return false; + const escapedUsername = normalizedUser.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`(^|[^A-Za-z0-9_-])@${escapedUsername}(?=$|[^A-Za-z0-9_-])`, 'iu').test(commentBody); +} /***/ }), @@ -63692,7 +63875,7 @@ function resolveThinkRequest(param) { if (command.kind === 'none') { if (!param.tokenUser?.trim()) return { kind: 'skip', reason: 'missing-token' }; - if (!commentBody.includes(`@${param.tokenUser}`)) + if (!(0, think_input_policy_1.containsBotMention)(commentBody, param.tokenUser)) return { kind: 'skip', reason: 'not-mentioned' }; } const question = command.kind === 'command' @@ -63936,6 +64119,7 @@ const project_context_instruction_1 = __nccwpck_require__(63907); const task_emoji_1 = __nccwpck_require__(46103); const agent_answer_policy_1 = __nccwpck_require__(72063); const github_comment_publication_policy_1 = __nccwpck_require__(72712); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); const TASK_ID = 'AnswerIssueHelpUseCase'; /** Posts one contextual answer for a newly opened question/help issue. */ async function runAnswerIssueHelpWorkflow(param, dependencies) { @@ -63966,9 +64150,17 @@ async function runAnswerIssueHelpWorkflow(param, dependencies) { if (!answer) { return [noAnswerResult()]; } - await dependencies.issueNotificationPort.addComment(param.owner, param.repo, issueNumber, answer, param.tokens.token); + const publishedAnswer = isNewIssue(param) + ? `${(0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(param.tokenUser)}\n\n${answer}` + : answer; + await dependencies.issueNotificationPort.addComment(param.owner, param.repo, issueNumber, publishedAnswer, param.tokens.token); (0, logging_ports_1.logInfo)(`Initial help reply posted to issue #${issueNumber}.`); - return [new result_1.Result({ id: TASK_ID, success: true, executed: true })]; + return [new result_1.Result({ + id: TASK_ID, + success: true, + executed: true, + payload: { welcomePublished: isNewIssue(param) }, + })]; } catch (error) { (0, logging_ports_1.logError)(`Error in ${TASK_ID}: ${error}`); @@ -63980,6 +64172,9 @@ async function runAnswerIssueHelpWorkflow(param, dependencies) { })]; } } +function isNewIssue(param) { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} function resolveHelpRequest(param) { if (!param.issue.opened || (!param.labels.isQuestion && !param.labels.isHelp)) return undefined; @@ -69086,17 +69281,23 @@ exports.Workflows = Workflows; /***/ }), /***/ 34737: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.authorizationForFileModification = authorizationForFileModification; +const github_user_policy_1 = __nccwpck_require__(84403); function authorizationForFileModification(owner, actor, ownerType) { if (ownerType === 'Organization') { return { kind: 'organization-membership', organization: owner, actor }; } - return { kind: 'owner', allowed: actor === owner }; + return { + kind: 'user-repository-collaborator', + owner, + actor, + ownerMatches: (0, github_user_policy_1.githubUsersMatch)(actor, owner), + }; } @@ -71946,17 +72147,20 @@ const actor_modification_policy_1 = __nccwpck_require__(34737); class ActorAuthorizationRepository { constructor(githubClient) { this.githubClient = githubClient; - this.isActorAllowedToModifyFiles = async (owner, actor, token) => { + this.isActorAllowedToModifyFiles = async (owner, repo, actor, token) => { try { const octokit = this.githubClient.getClient(token); const { data: ownerUser } = await octokit.rest.users.getByUsername({ username: owner }); const authorization = (0, actor_modification_policy_1.authorizationForFileModification)(owner, actor, ownerUser.type); - if (authorization.kind === 'owner') - return authorization.allowed; - return this.checkOrganizationMembership(octokit, authorization.organization, authorization.actor, owner, actor); + if (authorization.kind === 'organization-membership') { + return this.checkOrganizationMembership(octokit, authorization.organization, authorization.actor, owner, actor); + } + if (authorization.ownerMatches) + return true; + return this.checkUserRepositoryPermission(octokit, owner, actor, repo); } catch (err) { - (0, logger_1.logDebugInfo)(`isActorAllowedToModifyFiles(${owner}, ${actor}): ${err instanceof Error ? err.message : String(err)}`); + (0, logger_1.logDebugInfo)(`isActorAllowedToModifyFiles(${owner}, ${repo}, ${actor}): ${err instanceof Error ? err.message : String(err)}`); return false; } }; @@ -71967,15 +72171,31 @@ class ActorAuthorizationRepository { return true; } catch (membershipErr) { - const status = membershipErr?.status; - if (status === 404) - return false; - (0, logger_1.logDebugInfo)(`checkMembershipForUser(${owner}, ${originalActor}): ${membershipErr instanceof Error ? membershipErr.message : String(membershipErr)}`); + logUnlessNotFound(membershipErr, `checkMembershipForUser(${owner}, ${originalActor})`); + return false; + } + } + async checkUserRepositoryPermission(octokit, owner, actor, repo) { + try { + const response = await octokit.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username: actor, + }); + return ['admin', 'maintain', 'push'].includes(response.data.permission ?? ''); + } + catch (permissionErr) { + logUnlessNotFound(permissionErr, `getCollaboratorPermissionLevel(${owner}, ${repo}, ${actor})`); return false; } } } exports.ActorAuthorizationRepository = ActorAuthorizationRepository; +function logUnlessNotFound(error, operation) { + if (error?.status === 404) + return; + (0, logger_1.logDebugInfo)(`${operation}: ${error instanceof Error ? error.message : String(error)}`); +} /***/ }), @@ -74223,17 +74443,22 @@ exports.COPILOT_COMMAND_NAMES = void 0; exports.parseCopilotCommand = parseCopilotCommand; /** Explicit commands are the safe, deterministic entry point for mutations. */ exports.COPILOT_COMMAND_NAMES = [ + 'help', + 'analyze', 'plan', 'clarify', 'estimate', 'test-plan', 'status', 'description', + 'explain', + 'diagnose', 'review', 'findings', 'fix', 'dismiss', 'recheck', + 'implement', ]; const COMMAND_PREFIX = /^\/copilot(?:\s+|$)/iu; const MAX_COMMAND_LENGTH = 2000; @@ -74260,8 +74485,8 @@ function parseCopilotCommand(raw) { if (tokens.length > MAX_ARGUMENTS) { return { kind: 'invalid', reason: `Copilot commands accept at most ${MAX_ARGUMENTS} arguments.` }; } - if ((name === 'fix' || name === 'dismiss') && tokens.length === 0) { - return { kind: 'invalid', reason: `/${name} requires at least one finding id.` }; + if ((name === 'fix' || name === 'dismiss' || name === 'implement') && tokens.length === 0) { + return { kind: 'invalid', reason: `/${name} requires at least one argument.` }; } return { kind: 'command', @@ -76936,10 +77161,10 @@ function getBugbotFixPrompt(params) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getBugbotFixIntentPrompt = getBugbotFixIntentPrompt; /** - * Prompt for detecting if user comment is a fix request and which finding ids to target. + * Prompt for detecting the action requested by a user comment. */ const fill_1 = __nccwpck_require__(2559); -const TEMPLATE = `You are analyzing a user comment on an issue or pull request to decide whether they are asking to fix one or more reported code findings (bugs, vulnerabilities, or quality issues). +const TEMPLATE = `You are analyzing a user comment on an issue or pull request to classify the requested Copilot action. The available actions are: fix reported findings, apply a general repository change, run a read-only code review, or answer a question. {{projectContextInstruction}} @@ -76953,8 +77178,9 @@ const TEMPLATE = `You are analyzing a user comment on an issue or pull request t 1. Is this comment clearly a request to fix one or more of the findings above? (e.g. "fix it", "arreglalo", "fix this", "fix all", "fix vulnerability X", "corrige", "fix the bug in src/foo.ts"). If the user is asking a question, discussing something else, or the intent is ambiguous, set \`is_fix_request\` to false. 2. If it is a fix request, which finding ids should be fixed? Return their exact ids in \`target_finding_ids\`. If the user says "fix all" or equivalent, include every id from the list above. If they refer to a specific finding (e.g. by replying to a comment that contains one finding), return only that finding's id. Use only ids that appear in the list above. 3. Is the user asking to perform some other change or task in the repo? (e.g. "add a test for X", "refactor this", "implement feature Y", "haz que Z"). If yes, set \`is_do_request\` to true. Set false for pure questions or when the only intent is to fix the listed findings. +4. Is the user asking for a read-only review or analysis of the current code? (e.g. "analyze the changes for security issues", "review this PR for bugs", "look for performance problems"). If yes, set \`is_review_request\` to true. Do not set it for a question about how the code works or for a request that changes files. -Respond with a JSON object: \`is_fix_request\` (boolean), \`target_finding_ids\` (array of strings; empty when \`is_fix_request\` is false), and \`is_do_request\` (boolean).`; +Respond with a JSON object: \`is_fix_request\` (boolean), \`target_finding_ids\` (array of strings; empty when \`is_fix_request\` is false), \`is_do_request\` (boolean), and \`is_review_request\` (boolean).`; function getBugbotFixIntentPrompt(params) { return (0, fill_1.fillTemplate)(TEMPLATE, params); } diff --git a/build/cli/src/application/policies/copilot_interaction_policy.d.ts b/build/cli/src/application/policies/copilot_interaction_policy.d.ts new file mode 100644 index 00000000..38384aeb --- /dev/null +++ b/build/cli/src/application/policies/copilot_interaction_policy.d.ts @@ -0,0 +1,11 @@ +import { Result } from '../../data/model/result'; +export declare const DEFAULT_COPILOT_BOT_USERNAME = "vypbot"; +export declare const COPILOT_WELCOME_MARKER = ""; +/** Keeps the bot identity safe when it is rendered into a GitHub comment. */ +export declare function normalizeCopilotBotUsername(username: string | undefined): string; +/** Renders the stable command reference used by /copilot help. */ +export declare function buildCopilotHelpMessage(username?: string): string; +/** Renders the one-time onboarding comment for a newly created issue. */ +export declare function buildCopilotWelcomeMessage(username?: string): string; +/** Creates a publishable result for issues that have no agent-generated reply. */ +export declare function buildCopilotWelcomeResult(username?: string): Result; diff --git a/build/cli/src/application/ports/actor_authorization_ports.d.ts b/build/cli/src/application/ports/actor_authorization_ports.d.ts index cdd33510..cbc7d5c8 100644 --- a/build/cli/src/application/ports/actor_authorization_ports.d.ts +++ b/build/cli/src/application/ports/actor_authorization_ports.d.ts @@ -1,3 +1,3 @@ export interface ActorAuthorizationPort { - isActorAllowedToModifyFiles(owner: string, actor: string, token: string): Promise; + isActorAllowedToModifyFiles(owner: string, repository: string, actor: string, token: string): Promise; } diff --git a/build/cli/src/application/usecases/comment_automation_action_workflow.d.ts b/build/cli/src/application/usecases/comment_automation_action_workflow.d.ts index 4c0e2363..f83d0b38 100644 --- a/build/cli/src/application/usecases/comment_automation_action_workflow.d.ts +++ b/build/cli/src/application/usecases/comment_automation_action_workflow.d.ts @@ -5,7 +5,7 @@ import type { BugbotFindingResolutionPorts } from "../ports/bugbot_finding_resol import type { GitCommitPort } from "../ports/git_ports"; import type { CommentAutomationOptions } from "./comment_automation_contracts"; import type { BugbotFixIntentPayload } from "./steps/commit/bugbot/bugbot_fix_intent_payload"; -export type CommentAutomationAction = "autofix" | "do-user-request" | "think"; +export type CommentAutomationAction = "autofix" | "do-user-request" | "review" | "think"; export interface CommentAutomationActionPorts { authenticatedUserPort: AuthenticatedUserPort; bugbotResolutionPorts: BugbotFindingResolutionPorts; diff --git a/build/cli/src/application/usecases/comment_automation_route_policy.d.ts b/build/cli/src/application/usecases/comment_automation_route_policy.d.ts index 6d5f30c2..c8e5ef71 100644 --- a/build/cli/src/application/usecases/comment_automation_route_policy.d.ts +++ b/build/cli/src/application/usecases/comment_automation_route_policy.d.ts @@ -1,3 +1,3 @@ import type { BugbotFixIntentPayload } from './steps/commit/bugbot/bugbot_fix_intent_payload'; -export type CommentAutomationRoute = 'autofix' | 'do-user-request' | 'think'; -export declare function resolveCommentAutomationRoute(payload: BugbotFixIntentPayload | undefined, allowedToModifyFiles: boolean): CommentAutomationRoute; +export type CommentAutomationRoute = 'autofix' | 'do-user-request' | 'review' | 'think'; +export declare function resolveCommentAutomationRoute(payload: BugbotFixIntentPayload | undefined, allowedToModifyFiles: boolean, botMentioned?: boolean, explicitMutationCommand?: boolean): CommentAutomationRoute; diff --git a/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts b/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts index ab666eda..d7e8a218 100644 --- a/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts +++ b/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts @@ -3,6 +3,8 @@ export interface BugbotFixIntent { isFixRequest: boolean; isDoRequest: boolean; targetFindingIds: string[]; + isReviewRequest?: boolean; + requestText?: string; } export interface BugbotCommentSources { issue: { diff --git a/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts b/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts index a3d7e086..e8541048 100644 --- a/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts +++ b/build/cli/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts @@ -8,5 +8,5 @@ export interface DetectBugbotFixIntentWorkflowPorts { aiRepository: FindingsQueryPort; contextPorts: BugbotContextPorts; } -/** Detects whether a comment targets Bugbot findings and returns the validated intent payload. */ +/** Detects whether a comment requests a finding fix, repository change, or read-only review. */ export declare function runDetectBugbotFixIntentWorkflow(param: Execution, ports: DetectBugbotFixIntentWorkflowPorts): Promise; diff --git a/build/cli/src/application/usecases/steps/commit/bugbot/schema.d.ts b/build/cli/src/application/usecases/steps/commit/bugbot/schema.d.ts index e5725b17..8093be1b 100644 --- a/build/cli/src/application/usecases/steps/commit/bugbot/schema.d.ts +++ b/build/cli/src/application/usecases/steps/commit/bugbot/schema.d.ts @@ -76,9 +76,9 @@ export declare const BUGBOT_RESPONSE_SCHEMA: { readonly additionalProperties: false; }; /** - * Findings-agent response schema for bugbot fix intent. + * Findings-agent response schema for comment intent. * Given the user comment and the list of unresolved findings, the agent decides whether - * the user is asking to fix one or more of them and which finding ids to target. + * the user is asking to fix findings, apply a general change, or run a read-only review. */ export declare const BUGBOT_FIX_INTENT_RESPONSE_SCHEMA: { readonly type: "object"; @@ -98,7 +98,11 @@ export declare const BUGBOT_FIX_INTENT_RESPONSE_SCHEMA: { readonly type: "boolean"; readonly description: "True if the user is asking to perform some change or task in the repository (e.g. \"add a test for X\", \"refactor this\", \"implement feature Y\"). False for pure questions or when the only intent is to fix the reported findings (use is_fix_request for that)."; }; + readonly is_review_request: { + readonly type: "boolean"; + readonly description: "True if the user is asking for a read-only analysis or review of the current issue, branch, or pull request (e.g. \"analyze the changes for security issues\", \"review this PR for bugs\"). False for pure questions or file-changing requests."; + }; }; - readonly required: readonly ["is_fix_request", "target_finding_ids", "is_do_request"]; + readonly required: readonly ["is_fix_request", "target_finding_ids", "is_do_request", "is_review_request"]; readonly additionalProperties: false; }; diff --git a/build/cli/src/application/usecases/steps/common/think_input_policy.d.ts b/build/cli/src/application/usecases/steps/common/think_input_policy.d.ts index 33501bb0..d8a0a11d 100644 --- a/build/cli/src/application/usecases/steps/common/think_input_policy.d.ts +++ b/build/cli/src/application/usecases/steps/common/think_input_policy.d.ts @@ -6,3 +6,5 @@ export interface ThinkCommentSource { } export declare function getThinkCommentBody(source: ThinkCommentSource): string; export declare function extractMentionQuestion(commentBody: string, tokenUser: string): string; +/** Matches GitHub usernames case-insensitively without matching a larger username. */ +export declare function containsBotMention(commentBody: string, tokenUser: string): boolean; diff --git a/build/cli/src/data/repository/actor_modification_policy.d.ts b/build/cli/src/data/repository/actor_modification_policy.d.ts index 29eea824..c73af23c 100644 --- a/build/cli/src/data/repository/actor_modification_policy.d.ts +++ b/build/cli/src/data/repository/actor_modification_policy.d.ts @@ -1,9 +1,11 @@ export type ModificationAuthorization = { - kind: 'owner'; - allowed: boolean; -} | { kind: 'organization-membership'; organization: string; actor: string; +} | { + kind: 'user-repository-collaborator'; + owner: string; + actor: string; + ownerMatches: boolean; }; export declare function authorizationForFileModification(owner: string, actor: string, ownerType: string): ModificationAuthorization; diff --git a/build/cli/src/data/repository/organization/actor_authorization_repository.d.ts b/build/cli/src/data/repository/organization/actor_authorization_repository.d.ts index cdf4a082..44f2ff25 100644 --- a/build/cli/src/data/repository/organization/actor_authorization_repository.d.ts +++ b/build/cli/src/data/repository/organization/actor_authorization_repository.d.ts @@ -4,6 +4,7 @@ import type { GithubActorAuthorizationClient } from "../../../infrastructure/git export declare class ActorAuthorizationRepository implements ActorAuthorizationPort { private readonly githubClient; constructor(githubClient: GithubClientPort); - isActorAllowedToModifyFiles: (owner: string, actor: string, token: string) => Promise; + isActorAllowedToModifyFiles: (owner: string, repo: string, actor: string, token: string) => Promise; private checkOrganizationMembership; + private checkUserRepositoryPermission; } diff --git a/build/cli/src/domain/copilot_command.d.ts b/build/cli/src/domain/copilot_command.d.ts index 6487f6f4..8ec49bfe 100644 --- a/build/cli/src/domain/copilot_command.d.ts +++ b/build/cli/src/domain/copilot_command.d.ts @@ -1,5 +1,5 @@ /** Explicit commands are the safe, deterministic entry point for mutations. */ -export declare const COPILOT_COMMAND_NAMES: readonly ["plan", "clarify", "estimate", "test-plan", "status", "description", "review", "findings", "fix", "dismiss", "recheck"]; +export declare const COPILOT_COMMAND_NAMES: readonly ["help", "analyze", "plan", "clarify", "estimate", "test-plan", "status", "description", "explain", "diagnose", "review", "findings", "fix", "dismiss", "recheck", "implement"]; export type CopilotCommandName = typeof COPILOT_COMMAND_NAMES[number]; export interface ParsedCopilotCommand { readonly name: CopilotCommandName; diff --git a/build/cli/src/infrastructure/github/ports/github_identity_provider_ports.d.ts b/build/cli/src/infrastructure/github/ports/github_identity_provider_ports.d.ts index 5772e594..0da60b89 100644 --- a/build/cli/src/infrastructure/github/ports/github_identity_provider_ports.d.ts +++ b/build/cli/src/infrastructure/github/ports/github_identity_provider_ports.d.ts @@ -41,6 +41,17 @@ export interface GithubActorAuthorizationClient { username: string; }): Promise; }; + repos: { + getCollaboratorPermissionLevel(parameters: { + owner: string; + repo: string; + username: string; + }): Promise<{ + data: { + permission?: string; + }; + }>; + }; }; } export interface GithubOrganizationMembersClient { diff --git a/build/github_action/index.js b/build/github_action/index.js index 5fca6beb..5e01aef2 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -52674,6 +52674,85 @@ function getFindingStateCounts(value) { } +/***/ }), + +/***/ 90108: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.COPILOT_WELCOME_MARKER = exports.DEFAULT_COPILOT_BOT_USERNAME = void 0; +exports.normalizeCopilotBotUsername = normalizeCopilotBotUsername; +exports.buildCopilotHelpMessage = buildCopilotHelpMessage; +exports.buildCopilotWelcomeMessage = buildCopilotWelcomeMessage; +exports.buildCopilotWelcomeResult = buildCopilotWelcomeResult; +const result_1 = __nccwpck_require__(73817); +exports.DEFAULT_COPILOT_BOT_USERNAME = 'vypbot'; +exports.COPILOT_WELCOME_MARKER = ''; +const SAFE_GITHUB_USERNAME = /^[A-Za-z0-9-]+$/u; +/** Keeps the bot identity safe when it is rendered into a GitHub comment. */ +function normalizeCopilotBotUsername(username) { + const candidate = username?.trim().replace(/^@/u, ''); + return candidate && SAFE_GITHUB_USERNAME.test(candidate) + ? candidate + : exports.DEFAULT_COPILOT_BOT_USERNAME; +} +/** Renders the stable command reference used by /copilot help. */ +function buildCopilotHelpMessage(username) { + const bot = normalizeCopilotBotUsername(username); + return `## Copilot commands + +I’m **@${bot}**, the repository assistant. Use these commands on an issue or pull request: + +### Read-only + +- \`/copilot help\` — show this command reference. +- \`/copilot plan\` — propose an implementation plan. +- \`/copilot clarify\` — identify missing information and assumptions. +- \`/copilot estimate\` — estimate scope and complexity. +- \`/copilot test-plan\` — propose a focused testing strategy. +- \`/copilot explain \` — explain code or behavior. +- \`/copilot diagnose\` — investigate a reported problem and suggest likely causes. +- \`/copilot analyze\` — review the current issue, branch, or pull request for potential problems. +- \`/copilot review\` — run the Bugbot review. +- \`/copilot findings\` — show potential findings from the current code. +- \`/copilot recheck\` — re-run the review and reconcile findings. +- \`/copilot description\` — refresh the pull-request description. +- \`/copilot status\` — show the current automation status. + +### Changes + +- \`/copilot fix \` — fix one reported finding. +- \`/copilot fix all\` — fix all unresolved findings. +- \`/copilot dismiss \` — dismiss a finding. +- \`/copilot implement \` — apply an explicitly requested repository change. + +You can also ask a question in natural language by mentioning **@${bot}**. File-changing commands are restricted to authorized maintainers, run the configured checks, and report the resulting changes.`; +} +/** Renders the one-time onboarding comment for a newly created issue. */ +function buildCopilotWelcomeMessage(username) { + const bot = normalizeCopilotBotUsername(username); + return `${exports.COPILOT_WELCOME_MARKER} + +Hi! I’m **@${bot}**, the Copilot assistant for this repository. + +I can answer questions, explain the codebase, propose implementation and test plans, review issues and pull requests for potential bugs or security problems, and help authorized maintainers apply changes. + +Try \`/copilot help\` to see the available commands, or mention **@${bot}** with your question.`; +} +/** Creates a publishable result for issues that have no agent-generated reply. */ +function buildCopilotWelcomeResult(username) { + return new result_1.Result({ + id: 'CopilotWelcomeUseCase', + success: true, + executed: true, + stepFormat: 'markdown', + steps: [buildCopilotWelcomeMessage(username)], + }); +} + + /***/ }), /***/ 8428: @@ -55080,6 +55159,7 @@ exports.buildRecommendationResult = buildRecommendationResult; const result_1 = __nccwpck_require__(73817); const recommendation_policy_1 = __nccwpck_require__(39410); const logging_ports_1 = __nccwpck_require__(6152); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); function buildRecommendationResult(param, taskId, response, issueDescriptionFingerprint, previousRecommendation, issueNumber) { const steps = extractRecommendationText(response); if (!steps) { @@ -55098,15 +55178,21 @@ function buildRecommendationResult(param, taskId, response, issueDescriptionFing recommendationFingerprint, recommendation: (0, recommendation_policy_1.limitStoredRecommendation)(steps), }; + const stepsWithWelcome = isNewIssue(param) + ? [(0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(param.tokenUser), '## Recommended implementation steps', steps] + : ['## Recommended implementation steps', steps]; return [new result_1.Result({ id: taskId, success: true, executed: true, stepFormat: 'markdown', - steps: ['## Recommended implementation steps', steps], + steps: stepsWithWelcome, payload: { issueNumber, recommendedSteps: steps, recommendationState }, })]; } +function isNewIssue(param) { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} function skipUnchangedRecommendation(param, previous, fingerprint, reason) { param.currentConfiguration.recommendationState = { ...previous, issueDescriptionFingerprint: fingerprint }; (0, logging_ports_1.logInfo)(`RecommendSteps: ${reason}; skipping recommendation comment.`); @@ -55483,40 +55569,62 @@ const commit_user_request_workflow_1 = __nccwpck_require__(43393); const logging_ports_1 = __nccwpck_require__(6152); /** Runs the selected mutating action and returns any result records it produces. */ async function runCommentAutomationAction(param, options, route, intentPayload, ports) { - if (route === "autofix" && intentPayload) { - (0, logging_ports_1.logInfo)("Running bugbot autofix."); - const autofixResults = await options.autofixUseCase.invoke({ - execution: param, - targetFindingIds: intentPayload.targetFindingIds, - userComment: options.userComment, - context: intentPayload.context, - branchOverride: intentPayload.branchOverride, - }); - const resolutionErrors = await (0, commit_autofix_and_resolve_workflow_1.commitAutofixAndResolveFindings)(param, intentPayload, autofixResults, ports.authenticatedUserPort, ports.bugbotResolutionPorts, ports.gitCommitPort); - if (resolutionErrors.length > 0) { - autofixResults.push(new result_1.Result({ - id: `${options.taskId}.AutofixPostflight`, + if (route === "review") + return runReviewAction(param, options); + if (route === "autofix") + return runAutofixAction(param, options, intentPayload, ports); + if (route === "do-user-request") + return runDoUserRequestAction(param, options, intentPayload, ports); + return []; +} +async function runReviewAction(param, options) { + if (!options.reviewPotentialProblemsUseCase) { + return [new result_1.Result({ + id: `${options.taskId}.Review`, success: false, - executed: true, - steps: [ - "Autofix postflight failed: commit/push or finding reconciliation did not complete.", - ], - errors: resolutionErrors, - })); - } - return autofixResults; + executed: false, + errors: ["Read-only review is not available in this composition."], + })]; } - if (route === "do-user-request" && intentPayload) { - (0, logging_ports_1.logInfo)("Running do user request."); - const doResults = await options.doUserRequestUseCase.invoke({ - execution: param, - userComment: options.userComment, - branchOverride: intentPayload.branchOverride, - }); - const commitResults = await (0, commit_user_request_workflow_1.commitUserRequestIfSuccessful)(param, intentPayload.branchOverride, doResults, ports.authenticatedUserPort, ports.gitCommitPort); - return [...doResults, ...commitResults]; + (0, logging_ports_1.logInfo)("Running natural-language read-only review."); + return options.reviewPotentialProblemsUseCase.invoke(param); +} +async function runAutofixAction(param, options, intentPayload, ports) { + if (!intentPayload) + return []; + (0, logging_ports_1.logInfo)("Running bugbot autofix."); + const autofixResults = await options.autofixUseCase.invoke({ + execution: param, + targetFindingIds: intentPayload.targetFindingIds, + userComment: options.userComment, + context: intentPayload.context, + branchOverride: intentPayload.branchOverride, + }); + const resolutionErrors = await (0, commit_autofix_and_resolve_workflow_1.commitAutofixAndResolveFindings)(param, intentPayload, autofixResults, ports.authenticatedUserPort, ports.bugbotResolutionPorts, ports.gitCommitPort); + if (resolutionErrors.length > 0) { + autofixResults.push(new result_1.Result({ + id: `${options.taskId}.AutofixPostflight`, + success: false, + executed: true, + steps: [ + "Autofix postflight failed: commit/push or finding reconciliation did not complete.", + ], + errors: resolutionErrors, + })); } - return []; + return autofixResults; +} +async function runDoUserRequestAction(param, options, intentPayload, ports) { + if (!intentPayload) + return []; + (0, logging_ports_1.logInfo)("Running do user request."); + const doResults = await options.doUserRequestUseCase.invoke({ + execution: param, + userComment: intentPayload.requestText?.trim() || options.userComment, + branchOverride: intentPayload.branchOverride, + }); + const commitResults = await (0, commit_user_request_workflow_1.commitUserRequestIfSuccessful)(param, intentPayload.branchOverride, doResults, ports.authenticatedUserPort, ports.gitCommitPort); + return [...doResults, ...commitResults]; } @@ -55532,20 +55640,32 @@ exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; const result_1 = __nccwpck_require__(73817); const status_command_policy_1 = __nccwpck_require__(3449); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); /** Executes deterministic /copilot commands without routing them through intent detection. */ async function runExplicitCommentCommand(param, options, command, actorAuthorizationPort) { + if (command.name === 'help') + return runHelpCommand(param, options); if (command.name === 'status') return [(0, status_command_policy_1.buildCopilotStatusResult)(param, options.taskId)]; if (command.name === 'dismiss') return runDismissCommand(param, options, command, actorAuthorizationPort); if (command.name === 'description') return runDescriptionCommand(param, options); - if (['review', 'findings', 'recheck'].includes(command.name)) + if (['analyze', 'review', 'findings', 'recheck'].includes(command.name)) return runReviewCommand(param, options, command); - if (command.name === 'fix') + if (command.name === 'fix' || command.name === 'implement') return undefined; return runThinkCommand(param, options, command); } +function runHelpCommand(param, options) { + return [new result_1.Result({ + id: `${options.taskId}.Help`, + success: true, + executed: true, + stepFormat: 'markdown', + steps: [(0, copilot_interaction_policy_1.buildCopilotHelpMessage)(param.tokenUser)], + })]; +} async function runDescriptionCommand(param, options) { if (!options.updatePullRequestDescriptionUseCase) { return [new result_1.Result({ @@ -55558,7 +55678,7 @@ async function runDescriptionCommand(param, options) { return options.updatePullRequestDescriptionUseCase.invokeExplicit(param); } async function runDismissCommand(param, options, command, actorAuthorizationPort) { - const allowed = await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.actor, param.tokens.token); + const allowed = await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.repo, param.actor, param.tokens.token); if (!allowed || !options.dismissBugbotFindingsUseCase) { return [new result_1.Result({ id: options.taskId, @@ -55658,11 +55778,16 @@ exports.resolveCommentAutomationDecision = resolveCommentAutomationDecision; const logging_ports_1 = __nccwpck_require__(6152); const bugbot_fix_intent_payload_1 = __nccwpck_require__(25734); const comment_automation_route_policy_1 = __nccwpck_require__(47058); +const think_input_policy_1 = __nccwpck_require__(59687); +const copilot_command_1 = __nccwpck_require__(11771); async function resolveCommentAutomationDecision(param, options, actorAuthorizationPort) { (0, logging_ports_1.logInfo)("Running bugbot fix intent detection (before Think)."); const intentResults = await options.intentUseCase.invoke(param); const intentPayload = (0, bugbot_fix_intent_payload_1.getBugbotFixIntentPayload)(intentResults); - const route = (0, comment_automation_route_policy_1.resolveCommentAutomationRoute)(intentPayload, await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.actor, param.tokens.token)); + const parsedCommand = (0, copilot_command_1.parseCopilotCommand)(options.userComment); + const explicitMutationCommand = parsedCommand.kind === 'command' + && (parsedCommand.command.name === 'fix' || parsedCommand.command.name === 'implement'); + const route = (0, comment_automation_route_policy_1.resolveCommentAutomationRoute)(intentPayload, await actorAuthorizationPort.isActorAllowedToModifyFiles(param.owner, param.repo, param.actor, param.tokens.token), (0, think_input_policy_1.containsBotMention)(options.userComment, param.tokenUser ?? ''), explicitMutationCommand); logIntent(intentPayload); return { intentResults, intentPayload, route }; } @@ -55708,7 +55833,11 @@ async function runNaturalLanguageCommentAutomation(param, options, actorAuthoriz Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveCommentAutomationRoute = resolveCommentAutomationRoute; const bugbot_fix_intent_payload_1 = __nccwpck_require__(25734); -function resolveCommentAutomationRoute(payload, allowedToModifyFiles) { +function resolveCommentAutomationRoute(payload, allowedToModifyFiles, botMentioned = false, explicitMutationCommand = false) { + if (!botMentioned && !explicitMutationCommand) + return 'think'; + if (botMentioned && payload?.isReviewRequest) + return 'review'; if (!allowedToModifyFiles) return 'think'; if ((0, bugbot_fix_intent_payload_1.canRunBugbotAutofix)(payload)) @@ -56244,6 +56373,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runIssueWorkflow = runIssueWorkflow; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); /** Coordinates issue lifecycle steps in their required sequential order. */ async function runIssueWorkflow(param, taskId, ports) { const results = []; @@ -56288,10 +56418,24 @@ async function runIssueWorkflow(param, taskId, ports) { } const recommendation = resolveIssueRecommendation(param, ports); if (recommendation) { - results.push(...(await recommendation.invoke(param))); + const recommendationResults = await recommendation.invoke(param); + results.push(...recommendationResults); + if (isNewIssue(param) && !containsWelcome(recommendationResults)) { + results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(param.tokenUser)); + } + } + else if (isNewIssue(param)) { + results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(param.tokenUser)); } return results; } +function containsWelcome(results) { + return results.some((result) => result.steps.some((step) => step.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER)) + || (0, result_1.getResultPayload)(result.payload)?.welcomePublished === true); +} +function isNewIssue(param) { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} function resolveIssueRecommendation(param, ports) { if (!param.issue.opened && !param.issue.descriptionEdited) return undefined; @@ -57493,13 +57637,14 @@ function parseBugbotFixIntentResponse(response, unresolvedFindingIds) { const payload = response; const isFixRequest = payload.is_fix_request === true; const isDoRequest = payload.is_do_request === true; + const isReviewRequest = payload.is_review_request === true; const requestedIds = Array.isArray(payload.target_finding_ids) ? payload.target_finding_ids.filter((id) => typeof id === "string") : []; const targetFindingIds = isFixRequest ? unique(requestedIds.filter((id) => unresolvedFindingIds.has(id))) : []; - return { isFixRequest, isDoRequest, targetFindingIds }; + return { isFixRequest, isDoRequest, targetFindingIds, isReviewRequest }; } function unique(values) { return [...new Set(values)]; @@ -57558,7 +57703,7 @@ const load_bugbot_context_use_case_1 = __nccwpck_require__(4050); const schema_1 = __nccwpck_require__(16808); const detect_bugbot_fix_intent_policy_1 = __nccwpck_require__(14796); const TASK_ID = "DetectBugbotFixIntentUseCase"; -/** Detects whether a comment targets Bugbot findings and returns the validated intent payload. */ +/** Detects whether a comment requests a finding fix, repository change, or read-only review. */ async function runDetectBugbotFixIntentWorkflow(param, ports) { const results = []; if (param.issueNumber <= 0 && param.pullRequest.number <= 0) { @@ -57572,7 +57717,8 @@ async function runDetectBugbotFixIntentWorkflow(param, ports) { } const explicitCommand = (0, copilot_command_1.parseCopilotCommand)(commentBody); const isExplicitFix = explicitCommand.kind === 'command' && explicitCommand.command.name === 'fix'; - if (!isExplicitFix && !(0, agent_1.isAgentConfigurationReady)(param.ai?.getAgentConfiguration("findings"))) { + const isExplicitImplement = explicitCommand.kind === 'command' && explicitCommand.command.name === 'implement'; + if (!isExplicitFix && !isExplicitImplement && !(0, agent_1.isAgentConfigurationReady)(param.ai?.getAgentConfiguration("findings"))) { (0, logging_ports_1.logInfo)("Agent not configured; skipping bugbot fix intent detection."); return results; } @@ -57589,14 +57735,33 @@ async function runDetectBugbotFixIntentWorkflow(param, ports) { : undefined; const context = await (0, load_bugbot_context_use_case_1.loadBugbotContext)(param, contextOptions, ports.contextPorts); const unresolvedWithBody = context.unresolvedFindingsWithBody ?? []; - if (unresolvedWithBody.length === 0) { - (0, logging_ports_1.logInfo)("No unresolved bugbot findings for this issue/PR; skipping bugbot fix intent detection."); - return results; - } const unresolvedIds = new Set(unresolvedWithBody.map((finding) => finding.id)); const unresolvedFindings = (0, detect_bugbot_fix_intent_policy_1.buildUnresolvedFindingSummaries)(unresolvedWithBody); const parentCommentBody = await resolveParentCommentBody(param, ports.pullRequestQueryPort); + if (isExplicitImplement) { + const requestText = explicitCommand.command.arguments.join(' ').trim(); + results.push(new result_1.Result({ + id: TASK_ID, + success: true, + executed: true, + steps: ['Explicit implement command selected the authorized repository-change route.'], + payload: { + isFixRequest: false, + isDoRequest: true, + isReviewRequest: false, + targetFindingIds: [], + requestText, + context, + branchOverride, + }, + })); + return results; + } if (explicitCommand.kind === 'command' && explicitCommand.command.name === 'fix') { + if (unresolvedIds.size === 0) { + (0, logging_ports_1.logInfo)("No unresolved bugbot findings for explicit fix command; skipping autofix."); + return results; + } const requestedIds = explicitCommand.command.arguments.includes('all') ? [...unresolvedIds] : explicitCommand.command.arguments.filter(id => unresolvedIds.has(id)); @@ -57635,7 +57800,12 @@ async function runDetectBugbotFixIntentWorkflow(param, ports) { success: true, executed: true, steps: ["Bugbot fix intent: no response; skipping autofix."], - payload: { isFixRequest: false, isDoRequest: false, targetFindingIds: [] }, + payload: { + isFixRequest: false, + isDoRequest: false, + isReviewRequest: false, + targetFindingIds: [], + }, })); return results; } @@ -58754,9 +58924,9 @@ exports.BUGBOT_RESPONSE_SCHEMA = { additionalProperties: false, }; /** - * Findings-agent response schema for bugbot fix intent. + * Findings-agent response schema for comment intent. * Given the user comment and the list of unresolved findings, the agent decides whether - * the user is asking to fix one or more of them and which finding ids to target. + * the user is asking to fix findings, apply a general change, or run a read-only review. */ exports.BUGBOT_FIX_INTENT_RESPONSE_SCHEMA = { type: 'object', @@ -58774,8 +58944,12 @@ exports.BUGBOT_FIX_INTENT_RESPONSE_SCHEMA = { type: 'boolean', description: 'True if the user is asking to perform some change or task in the repository (e.g. "add a test for X", "refactor this", "implement feature Y"). False for pure questions or when the only intent is to fix the reported findings (use is_fix_request for that).', }, + is_review_request: { + type: 'boolean', + description: 'True if the user is asking for a read-only analysis or review of the current issue, branch, or pull request (e.g. "analyze the changes for security issues", "review this PR for bugs"). False for pure questions or file-changing requests.', + }, }, - required: ['is_fix_request', 'target_finding_ids', 'is_do_request'], + required: ['is_fix_request', 'target_finding_ids', 'is_do_request', 'is_review_request'], additionalProperties: false, }; @@ -60445,6 +60619,7 @@ async function queryThinkAnswer(param, prompt, repository, agentTask) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getThinkCommentBody = getThinkCommentBody; exports.extractMentionQuestion = extractMentionQuestion; +exports.containsBotMention = containsBotMention; function getThinkCommentBody(source) { if (source.isIssueComment) return source.issueCommentBody ?? ''; @@ -60456,6 +60631,14 @@ function extractMentionQuestion(commentBody, tokenUser) { const escapedUsername = tokenUser.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return commentBody.replace(new RegExp(`@${escapedUsername}`, 'gi'), '').trim(); } +/** Matches GitHub usernames case-insensitively without matching a larger username. */ +function containsBotMention(commentBody, tokenUser) { + const normalizedUser = tokenUser.trim().replace(/^@/u, ''); + if (!normalizedUser) + return false; + const escapedUsername = normalizedUser.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`(^|[^A-Za-z0-9_-])@${escapedUsername}(?=$|[^A-Za-z0-9_-])`, 'iu').test(commentBody); +} /***/ }), @@ -60486,7 +60669,7 @@ function resolveThinkRequest(param) { if (command.kind === 'none') { if (!param.tokenUser?.trim()) return { kind: 'skip', reason: 'missing-token' }; - if (!commentBody.includes(`@${param.tokenUser}`)) + if (!(0, think_input_policy_1.containsBotMention)(commentBody, param.tokenUser)) return { kind: 'skip', reason: 'not-mentioned' }; } const question = command.kind === 'command' @@ -60730,6 +60913,7 @@ const project_context_instruction_1 = __nccwpck_require__(63907); const task_emoji_1 = __nccwpck_require__(46103); const agent_answer_policy_1 = __nccwpck_require__(72063); const github_comment_publication_policy_1 = __nccwpck_require__(72712); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); const TASK_ID = 'AnswerIssueHelpUseCase'; /** Posts one contextual answer for a newly opened question/help issue. */ async function runAnswerIssueHelpWorkflow(param, dependencies) { @@ -60760,9 +60944,17 @@ async function runAnswerIssueHelpWorkflow(param, dependencies) { if (!answer) { return [noAnswerResult()]; } - await dependencies.issueNotificationPort.addComment(param.owner, param.repo, issueNumber, answer, param.tokens.token); + const publishedAnswer = isNewIssue(param) + ? `${(0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(param.tokenUser)}\n\n${answer}` + : answer; + await dependencies.issueNotificationPort.addComment(param.owner, param.repo, issueNumber, publishedAnswer, param.tokens.token); (0, logging_ports_1.logInfo)(`Initial help reply posted to issue #${issueNumber}.`); - return [new result_1.Result({ id: TASK_ID, success: true, executed: true })]; + return [new result_1.Result({ + id: TASK_ID, + success: true, + executed: true, + payload: { welcomePublished: isNewIssue(param) }, + })]; } catch (error) { (0, logging_ports_1.logError)(`Error in ${TASK_ID}: ${error}`); @@ -60774,6 +60966,9 @@ async function runAnswerIssueHelpWorkflow(param, dependencies) { })]; } } +function isNewIssue(param) { + return param.eventName === 'issues' && param.inputs?.action === 'opened'; +} function resolveHelpRequest(param) { if (!param.issue.opened || (!param.labels.isQuestion && !param.labels.isHelp)) return undefined; @@ -64171,17 +64366,23 @@ exports.Workflows = Workflows; /***/ }), /***/ 34737: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.authorizationForFileModification = authorizationForFileModification; +const github_user_policy_1 = __nccwpck_require__(84403); function authorizationForFileModification(owner, actor, ownerType) { if (ownerType === 'Organization') { return { kind: 'organization-membership', organization: owner, actor }; } - return { kind: 'owner', allowed: actor === owner }; + return { + kind: 'user-repository-collaborator', + owner, + actor, + ownerMatches: (0, github_user_policy_1.githubUsersMatch)(actor, owner), + }; } @@ -67208,17 +67409,20 @@ const actor_modification_policy_1 = __nccwpck_require__(34737); class ActorAuthorizationRepository { constructor(githubClient) { this.githubClient = githubClient; - this.isActorAllowedToModifyFiles = async (owner, actor, token) => { + this.isActorAllowedToModifyFiles = async (owner, repo, actor, token) => { try { const octokit = this.githubClient.getClient(token); const { data: ownerUser } = await octokit.rest.users.getByUsername({ username: owner }); const authorization = (0, actor_modification_policy_1.authorizationForFileModification)(owner, actor, ownerUser.type); - if (authorization.kind === 'owner') - return authorization.allowed; - return this.checkOrganizationMembership(octokit, authorization.organization, authorization.actor, owner, actor); + if (authorization.kind === 'organization-membership') { + return this.checkOrganizationMembership(octokit, authorization.organization, authorization.actor, owner, actor); + } + if (authorization.ownerMatches) + return true; + return this.checkUserRepositoryPermission(octokit, owner, actor, repo); } catch (err) { - (0, logger_1.logDebugInfo)(`isActorAllowedToModifyFiles(${owner}, ${actor}): ${err instanceof Error ? err.message : String(err)}`); + (0, logger_1.logDebugInfo)(`isActorAllowedToModifyFiles(${owner}, ${repo}, ${actor}): ${err instanceof Error ? err.message : String(err)}`); return false; } }; @@ -67229,15 +67433,31 @@ class ActorAuthorizationRepository { return true; } catch (membershipErr) { - const status = membershipErr?.status; - if (status === 404) - return false; - (0, logger_1.logDebugInfo)(`checkMembershipForUser(${owner}, ${originalActor}): ${membershipErr instanceof Error ? membershipErr.message : String(membershipErr)}`); + logUnlessNotFound(membershipErr, `checkMembershipForUser(${owner}, ${originalActor})`); + return false; + } + } + async checkUserRepositoryPermission(octokit, owner, actor, repo) { + try { + const response = await octokit.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username: actor, + }); + return ['admin', 'maintain', 'push'].includes(response.data.permission ?? ''); + } + catch (permissionErr) { + logUnlessNotFound(permissionErr, `getCollaboratorPermissionLevel(${owner}, ${repo}, ${actor})`); return false; } } } exports.ActorAuthorizationRepository = ActorAuthorizationRepository; +function logUnlessNotFound(error, operation) { + if (error?.status === 404) + return; + (0, logger_1.logDebugInfo)(`${operation}: ${error instanceof Error ? error.message : String(error)}`); +} /***/ }), @@ -69417,17 +69637,22 @@ exports.COPILOT_COMMAND_NAMES = void 0; exports.parseCopilotCommand = parseCopilotCommand; /** Explicit commands are the safe, deterministic entry point for mutations. */ exports.COPILOT_COMMAND_NAMES = [ + 'help', + 'analyze', 'plan', 'clarify', 'estimate', 'test-plan', 'status', 'description', + 'explain', + 'diagnose', 'review', 'findings', 'fix', 'dismiss', 'recheck', + 'implement', ]; const COMMAND_PREFIX = /^\/copilot(?:\s+|$)/iu; const MAX_COMMAND_LENGTH = 2000; @@ -69454,8 +69679,8 @@ function parseCopilotCommand(raw) { if (tokens.length > MAX_ARGUMENTS) { return { kind: 'invalid', reason: `Copilot commands accept at most ${MAX_ARGUMENTS} arguments.` }; } - if ((name === 'fix' || name === 'dismiss') && tokens.length === 0) { - return { kind: 'invalid', reason: `/${name} requires at least one finding id.` }; + if ((name === 'fix' || name === 'dismiss' || name === 'implement') && tokens.length === 0) { + return { kind: 'invalid', reason: `/${name} requires at least one argument.` }; } return { kind: 'command', @@ -71637,10 +71862,10 @@ function getBugbotFixPrompt(params) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getBugbotFixIntentPrompt = getBugbotFixIntentPrompt; /** - * Prompt for detecting if user comment is a fix request and which finding ids to target. + * Prompt for detecting the action requested by a user comment. */ const fill_1 = __nccwpck_require__(2559); -const TEMPLATE = `You are analyzing a user comment on an issue or pull request to decide whether they are asking to fix one or more reported code findings (bugs, vulnerabilities, or quality issues). +const TEMPLATE = `You are analyzing a user comment on an issue or pull request to classify the requested Copilot action. The available actions are: fix reported findings, apply a general repository change, run a read-only code review, or answer a question. {{projectContextInstruction}} @@ -71654,8 +71879,9 @@ const TEMPLATE = `You are analyzing a user comment on an issue or pull request t 1. Is this comment clearly a request to fix one or more of the findings above? (e.g. "fix it", "arreglalo", "fix this", "fix all", "fix vulnerability X", "corrige", "fix the bug in src/foo.ts"). If the user is asking a question, discussing something else, or the intent is ambiguous, set \`is_fix_request\` to false. 2. If it is a fix request, which finding ids should be fixed? Return their exact ids in \`target_finding_ids\`. If the user says "fix all" or equivalent, include every id from the list above. If they refer to a specific finding (e.g. by replying to a comment that contains one finding), return only that finding's id. Use only ids that appear in the list above. 3. Is the user asking to perform some other change or task in the repo? (e.g. "add a test for X", "refactor this", "implement feature Y", "haz que Z"). If yes, set \`is_do_request\` to true. Set false for pure questions or when the only intent is to fix the listed findings. +4. Is the user asking for a read-only review or analysis of the current code? (e.g. "analyze the changes for security issues", "review this PR for bugs", "look for performance problems"). If yes, set \`is_review_request\` to true. Do not set it for a question about how the code works or for a request that changes files. -Respond with a JSON object: \`is_fix_request\` (boolean), \`target_finding_ids\` (array of strings; empty when \`is_fix_request\` is false), and \`is_do_request\` (boolean).`; +Respond with a JSON object: \`is_fix_request\` (boolean), \`target_finding_ids\` (array of strings; empty when \`is_fix_request\` is false), \`is_do_request\` (boolean), and \`is_review_request\` (boolean).`; function getBugbotFixIntentPrompt(params) { return (0, fill_1.fillTemplate)(TEMPLATE, params); } diff --git a/build/github_action/src/application/policies/copilot_interaction_policy.d.ts b/build/github_action/src/application/policies/copilot_interaction_policy.d.ts new file mode 100644 index 00000000..38384aeb --- /dev/null +++ b/build/github_action/src/application/policies/copilot_interaction_policy.d.ts @@ -0,0 +1,11 @@ +import { Result } from '../../data/model/result'; +export declare const DEFAULT_COPILOT_BOT_USERNAME = "vypbot"; +export declare const COPILOT_WELCOME_MARKER = ""; +/** Keeps the bot identity safe when it is rendered into a GitHub comment. */ +export declare function normalizeCopilotBotUsername(username: string | undefined): string; +/** Renders the stable command reference used by /copilot help. */ +export declare function buildCopilotHelpMessage(username?: string): string; +/** Renders the one-time onboarding comment for a newly created issue. */ +export declare function buildCopilotWelcomeMessage(username?: string): string; +/** Creates a publishable result for issues that have no agent-generated reply. */ +export declare function buildCopilotWelcomeResult(username?: string): Result; diff --git a/build/github_action/src/application/ports/actor_authorization_ports.d.ts b/build/github_action/src/application/ports/actor_authorization_ports.d.ts index cdd33510..cbc7d5c8 100644 --- a/build/github_action/src/application/ports/actor_authorization_ports.d.ts +++ b/build/github_action/src/application/ports/actor_authorization_ports.d.ts @@ -1,3 +1,3 @@ export interface ActorAuthorizationPort { - isActorAllowedToModifyFiles(owner: string, actor: string, token: string): Promise; + isActorAllowedToModifyFiles(owner: string, repository: string, actor: string, token: string): Promise; } diff --git a/build/github_action/src/application/usecases/comment_automation_action_workflow.d.ts b/build/github_action/src/application/usecases/comment_automation_action_workflow.d.ts index 4c0e2363..f83d0b38 100644 --- a/build/github_action/src/application/usecases/comment_automation_action_workflow.d.ts +++ b/build/github_action/src/application/usecases/comment_automation_action_workflow.d.ts @@ -5,7 +5,7 @@ import type { BugbotFindingResolutionPorts } from "../ports/bugbot_finding_resol import type { GitCommitPort } from "../ports/git_ports"; import type { CommentAutomationOptions } from "./comment_automation_contracts"; import type { BugbotFixIntentPayload } from "./steps/commit/bugbot/bugbot_fix_intent_payload"; -export type CommentAutomationAction = "autofix" | "do-user-request" | "think"; +export type CommentAutomationAction = "autofix" | "do-user-request" | "review" | "think"; export interface CommentAutomationActionPorts { authenticatedUserPort: AuthenticatedUserPort; bugbotResolutionPorts: BugbotFindingResolutionPorts; diff --git a/build/github_action/src/application/usecases/comment_automation_route_policy.d.ts b/build/github_action/src/application/usecases/comment_automation_route_policy.d.ts index 6d5f30c2..c8e5ef71 100644 --- a/build/github_action/src/application/usecases/comment_automation_route_policy.d.ts +++ b/build/github_action/src/application/usecases/comment_automation_route_policy.d.ts @@ -1,3 +1,3 @@ import type { BugbotFixIntentPayload } from './steps/commit/bugbot/bugbot_fix_intent_payload'; -export type CommentAutomationRoute = 'autofix' | 'do-user-request' | 'think'; -export declare function resolveCommentAutomationRoute(payload: BugbotFixIntentPayload | undefined, allowedToModifyFiles: boolean): CommentAutomationRoute; +export type CommentAutomationRoute = 'autofix' | 'do-user-request' | 'review' | 'think'; +export declare function resolveCommentAutomationRoute(payload: BugbotFixIntentPayload | undefined, allowedToModifyFiles: boolean, botMentioned?: boolean, explicitMutationCommand?: boolean): CommentAutomationRoute; diff --git a/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts b/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts index ab666eda..d7e8a218 100644 --- a/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts +++ b/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_policy.d.ts @@ -3,6 +3,8 @@ export interface BugbotFixIntent { isFixRequest: boolean; isDoRequest: boolean; targetFindingIds: string[]; + isReviewRequest?: boolean; + requestText?: string; } export interface BugbotCommentSources { issue: { diff --git a/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts b/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts index a3d7e086..e8541048 100644 --- a/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts +++ b/build/github_action/src/application/usecases/steps/commit/bugbot/detect_bugbot_fix_intent_workflow.d.ts @@ -8,5 +8,5 @@ export interface DetectBugbotFixIntentWorkflowPorts { aiRepository: FindingsQueryPort; contextPorts: BugbotContextPorts; } -/** Detects whether a comment targets Bugbot findings and returns the validated intent payload. */ +/** Detects whether a comment requests a finding fix, repository change, or read-only review. */ export declare function runDetectBugbotFixIntentWorkflow(param: Execution, ports: DetectBugbotFixIntentWorkflowPorts): Promise; diff --git a/build/github_action/src/application/usecases/steps/commit/bugbot/schema.d.ts b/build/github_action/src/application/usecases/steps/commit/bugbot/schema.d.ts index e5725b17..8093be1b 100644 --- a/build/github_action/src/application/usecases/steps/commit/bugbot/schema.d.ts +++ b/build/github_action/src/application/usecases/steps/commit/bugbot/schema.d.ts @@ -76,9 +76,9 @@ export declare const BUGBOT_RESPONSE_SCHEMA: { readonly additionalProperties: false; }; /** - * Findings-agent response schema for bugbot fix intent. + * Findings-agent response schema for comment intent. * Given the user comment and the list of unresolved findings, the agent decides whether - * the user is asking to fix one or more of them and which finding ids to target. + * the user is asking to fix findings, apply a general change, or run a read-only review. */ export declare const BUGBOT_FIX_INTENT_RESPONSE_SCHEMA: { readonly type: "object"; @@ -98,7 +98,11 @@ export declare const BUGBOT_FIX_INTENT_RESPONSE_SCHEMA: { readonly type: "boolean"; readonly description: "True if the user is asking to perform some change or task in the repository (e.g. \"add a test for X\", \"refactor this\", \"implement feature Y\"). False for pure questions or when the only intent is to fix the reported findings (use is_fix_request for that)."; }; + readonly is_review_request: { + readonly type: "boolean"; + readonly description: "True if the user is asking for a read-only analysis or review of the current issue, branch, or pull request (e.g. \"analyze the changes for security issues\", \"review this PR for bugs\"). False for pure questions or file-changing requests."; + }; }; - readonly required: readonly ["is_fix_request", "target_finding_ids", "is_do_request"]; + readonly required: readonly ["is_fix_request", "target_finding_ids", "is_do_request", "is_review_request"]; readonly additionalProperties: false; }; diff --git a/build/github_action/src/application/usecases/steps/common/think_input_policy.d.ts b/build/github_action/src/application/usecases/steps/common/think_input_policy.d.ts index 33501bb0..d8a0a11d 100644 --- a/build/github_action/src/application/usecases/steps/common/think_input_policy.d.ts +++ b/build/github_action/src/application/usecases/steps/common/think_input_policy.d.ts @@ -6,3 +6,5 @@ export interface ThinkCommentSource { } export declare function getThinkCommentBody(source: ThinkCommentSource): string; export declare function extractMentionQuestion(commentBody: string, tokenUser: string): string; +/** Matches GitHub usernames case-insensitively without matching a larger username. */ +export declare function containsBotMention(commentBody: string, tokenUser: string): boolean; diff --git a/build/github_action/src/data/repository/actor_modification_policy.d.ts b/build/github_action/src/data/repository/actor_modification_policy.d.ts index 29eea824..c73af23c 100644 --- a/build/github_action/src/data/repository/actor_modification_policy.d.ts +++ b/build/github_action/src/data/repository/actor_modification_policy.d.ts @@ -1,9 +1,11 @@ export type ModificationAuthorization = { - kind: 'owner'; - allowed: boolean; -} | { kind: 'organization-membership'; organization: string; actor: string; +} | { + kind: 'user-repository-collaborator'; + owner: string; + actor: string; + ownerMatches: boolean; }; export declare function authorizationForFileModification(owner: string, actor: string, ownerType: string): ModificationAuthorization; diff --git a/build/github_action/src/data/repository/organization/actor_authorization_repository.d.ts b/build/github_action/src/data/repository/organization/actor_authorization_repository.d.ts index cdf4a082..44f2ff25 100644 --- a/build/github_action/src/data/repository/organization/actor_authorization_repository.d.ts +++ b/build/github_action/src/data/repository/organization/actor_authorization_repository.d.ts @@ -4,6 +4,7 @@ import type { GithubActorAuthorizationClient } from "../../../infrastructure/git export declare class ActorAuthorizationRepository implements ActorAuthorizationPort { private readonly githubClient; constructor(githubClient: GithubClientPort); - isActorAllowedToModifyFiles: (owner: string, actor: string, token: string) => Promise; + isActorAllowedToModifyFiles: (owner: string, repo: string, actor: string, token: string) => Promise; private checkOrganizationMembership; + private checkUserRepositoryPermission; } diff --git a/build/github_action/src/domain/copilot_command.d.ts b/build/github_action/src/domain/copilot_command.d.ts index 6487f6f4..8ec49bfe 100644 --- a/build/github_action/src/domain/copilot_command.d.ts +++ b/build/github_action/src/domain/copilot_command.d.ts @@ -1,5 +1,5 @@ /** Explicit commands are the safe, deterministic entry point for mutations. */ -export declare const COPILOT_COMMAND_NAMES: readonly ["plan", "clarify", "estimate", "test-plan", "status", "description", "review", "findings", "fix", "dismiss", "recheck"]; +export declare const COPILOT_COMMAND_NAMES: readonly ["help", "analyze", "plan", "clarify", "estimate", "test-plan", "status", "description", "explain", "diagnose", "review", "findings", "fix", "dismiss", "recheck", "implement"]; export type CopilotCommandName = typeof COPILOT_COMMAND_NAMES[number]; export interface ParsedCopilotCommand { readonly name: CopilotCommandName; diff --git a/build/github_action/src/infrastructure/github/ports/github_identity_provider_ports.d.ts b/build/github_action/src/infrastructure/github/ports/github_identity_provider_ports.d.ts index 5772e594..0da60b89 100644 --- a/build/github_action/src/infrastructure/github/ports/github_identity_provider_ports.d.ts +++ b/build/github_action/src/infrastructure/github/ports/github_identity_provider_ports.d.ts @@ -41,6 +41,17 @@ export interface GithubActorAuthorizationClient { username: string; }): Promise; }; + repos: { + getCollaboratorPermissionLevel(parameters: { + owner: string; + repo: string; + username: string; + }): Promise<{ + data: { + permission?: string; + }; + }>; + }; }; } export interface GithubOrganizationMembersClient { From 2b9b31d475d873a5853ce9aa2239f78e62b0cd95 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 4 Sep 2026 14:34:06 +0200 Subject: [PATCH 08/11] develop: support organization-scoped actions resources --- README.md | 2 +- build/cli/index.js | 719 +++++++++++++++++- .../policies/setup_configuration_policy.d.ts | 20 +- .../application/ports/setup_wizard_ports.d.ts | 25 +- .../actions/initial_setup_use_case.d.ts | 5 +- .../actions/initial_setup_workflow.d.ts | 3 +- .../usecases/setup/doctor_use_case.d.ts | 5 +- .../setup/setup_credentials_use_case.d.ts | 2 + .../usecases/setup/setup_wizard_use_case.d.ts | 16 +- build/cli/src/cli/commands/setup_policy.d.ts | 4 +- build/cli/src/cli/setup_prompt_adapter.d.ts | 11 +- .../repository_variables_repository.d.ts | 20 +- build/cli/src/domain/setup.d.ts | 36 + .../setup_credentials_composition_root.d.ts | 3 +- .../github_repository_variables_protocol.d.ts | 58 +- build/github_action/index.js | 427 ++++++++++- .../policies/setup_configuration_policy.d.ts | 20 +- .../application/ports/setup_wizard_ports.d.ts | 25 +- .../actions/initial_setup_use_case.d.ts | 5 +- .../actions/initial_setup_workflow.d.ts | 3 +- .../usecases/setup/doctor_use_case.d.ts | 5 +- .../setup/setup_credentials_use_case.d.ts | 2 + .../usecases/setup/setup_wizard_use_case.d.ts | 16 +- .../src/cli/commands/setup_policy.d.ts | 4 +- .../src/cli/setup_prompt_adapter.d.ts | 11 +- .../repository_variables_repository.d.ts | 20 +- build/github_action/src/domain/setup.d.ts | 36 + .../setup_credentials_composition_root.d.ts | 3 +- .../github_repository_variables_protocol.d.ts | 58 +- docs/authentication.mdx | 2 +- docs/configuration-checklist.mdx | 4 + docs/configuration.mdx | 23 + docs/single-actions/workflow-and-cli.mdx | 31 +- .../setup_configuration_policy.test.ts | 137 ++++ .../policies/setup_configuration_policy.ts | 187 ++++- src/application/ports/setup_wizard_ports.ts | 34 +- .../__tests__/initial_setup_use_case.test.ts | 39 + .../actions/initial_setup_use_case.ts | 8 +- .../actions/initial_setup_workflow.ts | 137 +++- .../setup/__tests__/doctor_use_case.test.ts | 65 ++ .../setup_credentials_use_case.test.ts | 26 + .../__tests__/setup_wizard_use_case.test.ts | 43 +- .../usecases/setup/doctor_use_case.ts | 77 +- .../setup/setup_credentials_use_case.ts | 25 +- .../usecases/setup/setup_wizard_use_case.ts | 66 +- src/cli/__tests__/setup_config_file.test.ts | 22 + src/cli/commands/setup.ts | 78 +- src/cli/commands/setup_policy.ts | 4 +- src/cli/setup_config_file.ts | 47 +- src/cli/setup_prompt_adapter.ts | 97 ++- .../repository_variables_repository.test.ts | 122 +++ .../repository_variables_repository.ts | 214 +++++- src/domain/setup.ts | 42 + .../initial_setup_composition_root.test.ts | 2 +- .../initial_setup_composition_root.ts | 1 + .../setup_credentials_composition_root.ts | 6 +- .../setup_doctor_composition_root.ts | 1 + .../github_repository_variables_protocol.ts | 36 +- 58 files changed, 2969 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 0e3de789..55ab3dfb 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Full documentation: **[docs.page/vypdev/copilot](https://docs.page/vypdev/copilo ## Getting started -1. **Create the workflow PAT** for the bot account and store it as a repo secret (e.g. `PAT`). `copilot setup` separately asks the operator for a setup PAT that is used only during local configuration. See [Authentication](https://docs.page/vypdev/copilot/authentication). +1. **Create the workflow PAT** for the bot account and store it as a repository or organization Secret (e.g. `PAT`). `copilot setup` separately asks the operator for a setup PAT that is used only during local configuration, and lets you choose repository or organization scope independently for Secrets and Variables. See [Authentication](https://docs.page/vypdev/copilot/authentication). 2. **Use the action** from the marketplace so versions are stable: ```yaml uses: vypdev/copilot@v3 diff --git a/build/cli/index.js b/build/cli/index.js index 8d5b4a45..ad5ad5a6 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -56785,6 +56785,7 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SETUP_FEATURE_DESCRIPTIONS = exports.SETUP_AGENT_TASKS = void 0; +exports.createDefaultSetupStorageConfiguration = createDefaultSetupStorageConfiguration; exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; exports.mergeSetupConfiguration = mergeSetupConfiguration; exports.validateSetupConfiguration = validateSetupConfiguration; @@ -56792,6 +56793,14 @@ exports.buildSetupPlan = buildSetupPlan; exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; exports.buildSetupActionInputs = buildSetupActionInputs; +exports.resolveSetupResourceScope = resolveSetupResourceScope; +exports.getSetupResourceStoragePolicy = getSetupResourceStoragePolicy; +exports.getSetupStorageConfiguration = getSetupStorageConfiguration; +exports.resolveSetupResourceTarget = resolveSetupResourceTarget; +exports.setupResourceExists = setupResourceExists; +exports.shouldUpsertSetupResource = shouldUpsertSetupResource; +exports.validateSetupStorageAgainstRemote = validateSetupStorageAgainstRemote; +exports.usesOrganizationStorage = usesOrganizationStorage; const agent_1 = __nccwpck_require__(89040); const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); const pull_request_description_1 = __nccwpck_require__(45315); @@ -56843,6 +56852,21 @@ const SECRET_BY_MODEL_PROVIDER = { google: 'GOOGLE_API_KEY', openrouter: 'OPENROUTER_API_KEY', }; +const RESOURCE_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/; +function defaultStoragePolicy() { + return { + defaultScope: 'repository', + organizationVisibility: 'selected', + preserveExisting: true, + overrides: {}, + }; +} +function createDefaultSetupStorageConfiguration() { + return { + secrets: defaultStoragePolicy(), + variables: defaultStoragePolicy(), + }; +} function createDefaultSetupConfiguration() { const defaultRole = () => ({ provider: agent_1.DEFAULT_AGENT_PROVIDER, @@ -56895,6 +56919,7 @@ function createDefaultSetupConfiguration() { manageRepositoryVariables: true, manageRepositorySecrets: true, actionInputs: {}, + storage: createDefaultSetupStorageConfiguration(), }; } function mergeSetupConfiguration(base, overrides = {}) { @@ -56913,6 +56938,10 @@ function mergeSetupConfiguration(base, overrides = {}) { manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, + storage: { + secrets: mergeStoragePolicy(base.storage?.secrets, overrides.storage?.secrets), + variables: mergeStoragePolicy(base.storage?.variables, overrides.storage?.variables), + }, }; } function validateSetupConfiguration(configuration) { @@ -56952,6 +56981,7 @@ function validateSetupConfiguration(configuration) { if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } + errors.push(...validateStorageConfiguration(configuration.storage)); for (const task of exports.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) @@ -57131,7 +57161,7 @@ function buildRequiredSetupSecrets(configuration) { function buildSetupWarnings(configuration) { const warnings = []; if (configuration.features.release !== false && configuration.features.hotfix !== false) { - warnings.push('Release and hotfix workflows require the repository secret PAT and a writable token.'); + warnings.push('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); } if (configuration.ai.provisioningMode === 'always') { warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); @@ -57142,8 +57172,128 @@ function buildSetupWarnings(configuration) { if (exports.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); } + if (usesOrganizationStorage(configuration)) { + warnings.push('Organization-level Secrets and Variables require organization permissions; selected access is the safest default and repository values take precedence.'); + } return warnings; } +function resolveSetupResourceScope(policy, name) { + return policy.overrides[name] ?? policy.defaultScope; +} +function getSetupResourceStoragePolicy(configuration, kind) { + return getSetupStorageConfiguration(configuration)[kind === 'secret' ? 'secrets' : 'variables']; +} +function getSetupStorageConfiguration(configuration) { + const fallback = createDefaultSetupStorageConfiguration(); + return { + secrets: mergeStoragePolicy(fallback.secrets, configuration.storage?.secrets), + variables: mergeStoragePolicy(fallback.variables, configuration.storage?.variables), + }; +} +function resolveSetupResourceTarget(configuration, kind, name, remote) { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + const existingScope = setupResourceExists(remote, kind, name).effective; + const scope = existingScope && policy.preserveExisting && !explicitOverride + ? existingScope + : resolveSetupResourceScope(policy, name); + return { + scope, + organizationVisibility: policy.organizationVisibility, + repositoryId: remote?.repositoryId, + }; +} +function setupResourceExists(remote, kind, name) { + if (!remote) + return { repository: false, organization: false }; + const repository = kind === 'secret' + ? remote.repositorySecrets.includes(name) + : remote.repositoryVariables.some(variable => variable.name === name); + const organizationAccess = kind === 'secret' + ? (remote.organizationSecretsAccess ?? remote.organizationAccess) + : (remote.organizationVariablesAccess ?? remote.organizationAccess); + const organization = organizationAccess === 'available' && (kind === 'secret' + ? remote.organizationSecrets.includes(name) + : remote.organizationVariables.some(variable => variable.name === name)); + return { + repository, + organization, + effective: repository ? 'repository' : organization ? 'organization' : undefined, + }; +} +function shouldUpsertSetupResource(configuration, kind, name, remote) { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const state = setupResourceExists(remote, kind, name); + if (!state.effective) + return true; + const requested = resolveSetupResourceScope(policy, name); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + return requested === state.effective || explicitOverride || !policy.preserveExisting; +} +function validateSetupStorageAgainstRemote(configuration, remote) { + const errors = []; + const policies = [ + ['secret', getSetupResourceStoragePolicy(configuration, 'secret'), configuration.manageRepositorySecrets], + ['variable', getSetupResourceStoragePolicy(configuration, 'variable'), configuration.manageRepositoryVariables], + ]; + for (const [kind, policy, managed] of policies) { + if (!managed) + continue; + const needsOrganization = policy.defaultScope === 'organization' + || Object.values(policy.overrides).includes('organization'); + if (!needsOrganization) + continue; + if (remote.ownerType !== 'Organization') { + errors.push(`Organization-level ${kind} storage is only available for organization-owned repositories.`); + continue; + } + const access = kind === 'secret' ? remote.organizationSecretsAccess : remote.organizationVariablesAccess; + if (access !== 'available') { + errors.push(`The setup PAT cannot inspect organization ${kind}s for this repository. Organization ${kind} permissions are required.`); + } + if (policy.organizationVisibility === 'selected' && remote.repositoryId === undefined) { + errors.push(`The repository ID is required for selected organization ${kind} access.`); + } + } + return errors; +} +function usesOrganizationStorage(configuration) { + const storage = getSetupStorageConfiguration(configuration); + return [storage.secrets, storage.variables].some(policy => policy.defaultScope === 'organization' || Object.values(policy.overrides).includes('organization')); +} +function mergeStoragePolicy(base, override) { + const fallback = base ?? defaultStoragePolicy(); + return { + ...fallback, + ...(override ?? {}), + overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, + }; +} +function validateStorageConfiguration(storage) { + // Setup files created before scoped storage was introduced remain valid and + // receive the repository-level defaults through getSetupStorageConfiguration. + if (!storage) + return []; + const errors = []; + for (const [kind, policy] of Object.entries(storage)) { + if (!policy || !['repository', 'organization'].includes(policy.defaultScope)) { + errors.push(`${kind} default scope must be repository or organization.`); + continue; + } + if (!['all', 'private', 'selected'].includes(policy.organizationVisibility)) { + errors.push(`${kind} organization visibility must be all, private, or selected.`); + } + if (typeof policy.preserveExisting !== 'boolean') + errors.push(`${kind} preserveExisting must be a boolean.`); + for (const [name, scope] of Object.entries(policy.overrides ?? {})) { + if (!RESOURCE_NAME_PATTERN.test(name)) + errors.push(`${kind} override name ${name} must be an uppercase GitHub Actions name.`); + if (!['repository', 'organization'].includes(scope)) + errors.push(`${kind} override ${name} must use repository or organization.`); + } + } + return errors; +} function unique(values) { return [...new Set(values.map(value => value.trim()).filter(Boolean))]; } @@ -57895,7 +58045,7 @@ exports.InitialSetupUseCase = void 0; const initial_setup_workflow_1 = __nccwpck_require__(18079); /** Application boundary for provisioning a repository for Copilot automation. */ class InitialSetupUseCase { - constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort) { + constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort, setupRemoteConfigurationReadPort) { this.authenticatedUserPort = authenticatedUserPort; this.initialLabelProvisioningPort = initialLabelProvisioningPort; this.issueTypeProvisioningPort = issueTypeProvisioningPort; @@ -57905,6 +58055,7 @@ class InitialSetupUseCase { this.setupWorkspacePort = setupWorkspacePort; this.setupRepositoryVariablesPort = setupRepositoryVariablesPort; this.setupRepositorySecretsPort = setupRepositorySecretsPort; + this.setupRemoteConfigurationReadPort = setupRemoteConfigurationReadPort; this.taskId = 'InitialSetupUseCase'; } async invoke(param) { @@ -57918,6 +58069,7 @@ class InitialSetupUseCase { setupWorkspacePort: this.setupWorkspacePort, setupRepositoryVariablesPort: this.setupRepositoryVariablesPort, setupRepositorySecretsPort: this.setupRepositorySecretsPort, + setupRemoteConfigurationReadPort: this.setupRemoteConfigurationReadPort, }); } } @@ -57969,7 +58121,8 @@ async function runInitialSetupWorkflow(param, dependencies) { return [buildResult(errors, steps)]; } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); - const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration); + const remoteConfiguration = await resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors); + const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration); if (secrets.step) steps.push(secrets.step); if (secrets.errors.length > 0) @@ -57991,7 +58144,7 @@ async function runInitialSetupWorkflow(param, dependencies) { else { steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration); + const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration); if (variables.step) steps.push(variables.step); if (variables.errors.length > 0) @@ -58083,16 +58236,18 @@ function getWorkflowUpdates(param) { const updates = param.inputs?.setupWorkflowUpdates; return Array.isArray(updates) ? updates.filter((file) => typeof file === 'string') : []; } -async function ensureRepositoryVariables(param, dependencies, setupConfiguration) { +async function ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration) { if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { return { errors: [] }; } try { - const result = await dependencies.setupRepositoryVariablesPort.upsert(param.owner, param.repo, param.tokens.token, (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration)); + const desired = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration); + const groups = groupResources(desired, 'variable', setupConfiguration, remoteConfiguration); + const result = await upsertVariableGroups(param, dependencies.setupRepositoryVariablesPort, groups); if (result.errors.length > 0) return { errors: result.errors }; return { - step: `✅ Repository Variables: ${result.created} created, ${result.updated} updated`, + step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, errors: [], }; } @@ -58102,7 +58257,7 @@ async function ensureRepositoryVariables(param, dependencies, setupConfiguration return { errors: [message] }; } } -async function ensureRepositorySecrets(param, dependencies, setupConfiguration) { +async function ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration) { if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { return { errors: [] }; } @@ -58117,11 +58272,12 @@ async function ensureRepositorySecrets(param, dependencies, setupConfiguration) if (values.length === 0) return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; try { - const result = await dependencies.setupRepositorySecretsPort.upsertSecrets(param.owner, param.repo, param.tokens.token, values); + const groups = groupResources(values, 'secret', setupConfiguration, remoteConfiguration); + const result = await upsertSecretGroups(param, dependencies.setupRepositorySecretsPort, groups); if (result.errors.length > 0) return { errors: result.errors }; return { - step: `✅ Repository Secrets: ${result.created} created, ${result.updated} updated; existing values kept when no replacement was selected.`, + step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, errors: [], }; } @@ -58131,6 +58287,77 @@ async function ensureRepositorySecrets(param, dependencies, setupConfiguration) return { errors: [message] }; } } +async function resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors) { + const provided = param.inputs?.setupRemoteConfiguration; + if (provided && typeof provided === 'object') + return provided; + if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) + return undefined; + try { + return await dependencies.setupRemoteConfigurationReadPort.inspect(param.owner, param.repo, param.tokens.token); + } + catch (error) { + const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + (0, logging_ports_1.logError)(message); + if ((0, setup_configuration_policy_1.usesOrganizationStorage)(setupConfiguration)) + errors.push(message); + return undefined; + } +} +function groupResources(resources, kind, configuration, remoteConfiguration) { + const groups = new Map(); + for (const resource of resources) { + // Secret values reach this workflow only after the user chose keep/replace. + // Variables, however, are always generated from the selected setup contract, + // so preserveExisting must be applied here to avoid shadowing inherited values. + if (kind === 'variable' && !(0, setup_configuration_policy_1.shouldUpsertSetupResource)(configuration, kind, resource.name, remoteConfiguration)) + continue; + const target = (0, setup_configuration_policy_1.resolveSetupResourceTarget)(configuration, kind, resource.name, remoteConfiguration); + const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; + const group = groups.get(key) ?? { target, resources: [] }; + group.resources.push(resource); + groups.set(key, group); + } + return [...groups.values()]; +} +async function upsertVariableGroups(param, port, groups) { + let created = 0; + let updated = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedVariables) { + errors.push('Organization Variable provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedVariables(param.owner, param.repo, param.tokens.token, group.target, group.resources) + : await port.upsert(param.owner, param.repo, param.tokens.token, group.resources); + created += result.created; + updated += result.updated; + errors.push(...result.errors); + } + return { created, updated, errors }; +} +async function upsertSecretGroups(param, port, groups) { + let created = 0; + let updated = 0; + let skipped = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { + errors.push('Organization Secret provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedSecrets(param.owner, param.repo, param.tokens.token, group.target, group.resources) + : await port.upsertSecrets(param.owner, param.repo, param.tokens.token, group.resources); + created += result.created; + updated += result.updated; + skipped += result.skipped; + errors.push(...result.errors); + } + return { created, updated, skipped, errors }; +} function getSetupCredentialCollection(param) { const credentials = param.inputs?.setupCredentials; if (!credentials || typeof credentials !== 'object') @@ -59751,13 +59978,14 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupDoctorUseCase = void 0; const setup_configuration_policy_1 = __nccwpck_require__(56637); class SetupDoctorUseCase { - constructor(validation, secrets, variables, workspace, output, remoteHealth) { + constructor(validation, secrets, variables, workspace, output, remoteHealth, remoteConfigurationReader) { this.validation = validation; this.secrets = secrets; this.variables = variables; this.workspace = workspace; this.output = output; this.remoteHealth = remoteHealth; + this.remoteConfigurationReader = remoteConfigurationReader; } async execute(request) { const checks = []; @@ -59775,18 +60003,65 @@ class SetupDoctorUseCase { message: comparison.status === 'unchanged' ? 'Matches the installed setup template.' : `Local workflow is ${comparison.status}.`, }); } + let remoteConfiguration; + if (this.remoteConfigurationReader) { + try { + remoteConfiguration = await this.remoteConfigurationReader.inspect(request.owner, request.repository, request.setupToken); + } + catch (error) { + const message = `Could not inspect GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + checks.push({ + area: 'GitHub Actions scopes', + status: (0, setup_configuration_policy_1.usesOrganizationStorage)(request.configuration) ? 'fail' : 'warn', + message, + }); + } + } const requiredVariables = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(request.configuration); - const remoteVariables = await this.variables.listVariables(request.owner, request.repository, request.setupToken); - const remoteVariableMap = new Map(remoteVariables.map(variable => [variable.name, variable.value])); + const remoteVariables = remoteConfiguration?.repositoryVariables + ?? await this.variables.listVariables(request.owner, request.repository, request.setupToken); + const remoteVariableMap = new Map(remoteVariables.map(variable => [variable.name, { value: variable.value, source: 'repository' }])); + if (remoteConfiguration) { + for (const variable of remoteConfiguration.organizationVariables) { + if (!remoteVariableMap.has(variable.name)) { + remoteVariableMap.set(variable.name, { value: variable.value, source: 'organization' }); + } + } + } for (const variable of requiredVariables) { - const value = remoteVariableMap.get(variable.name); + const remoteVariable = remoteVariableMap.get(variable.name); + const value = remoteVariable?.value; + const state = (0, setup_configuration_policy_1.setupResourceExists)(remoteConfiguration, 'variable', variable.name); + const policy = (0, setup_configuration_policy_1.getSetupResourceStoragePolicy)(request.configuration, 'variable'); + const preserveExisting = state.effective !== undefined + && state.effective !== (0, setup_configuration_policy_1.resolveSetupResourceScope)(policy, variable.name) + && !Object.prototype.hasOwnProperty.call(policy.overrides, variable.name) + && policy.preserveExisting; + const sourceMessage = remoteVariable?.source === 'organization' + ? ' Variable is inherited from the organization scope.' + : remoteVariable + ? ' Variable is configured at repository scope.' + : ''; + const matches = value === variable.value; checks.push({ area: `Variable ${variable.name}`, - status: value === undefined ? 'fail' : value === variable.value ? 'pass' : 'fail', - message: value === undefined ? 'Variable is missing.' : value === variable.value ? 'Variable is configured.' : 'Variable exists but differs from the selected setup configuration.', + status: value === undefined ? 'fail' : matches ? 'pass' : preserveExisting ? 'warn' : 'fail', + message: value === undefined + ? 'Variable is missing.' + : matches + ? `Variable is configured.${sourceMessage}` + : preserveExisting + ? `Variable differs from the selected setup configuration but is preserved at ${remoteVariable?.source} scope.` + : 'Variable exists but differs from the selected setup configuration.', }); } - const remoteSecrets = new Set(await this.secrets.list(request.owner, request.repository, request.setupToken)); + const repositorySecretNames = remoteConfiguration?.repositorySecrets + ?? await this.secrets.list(request.owner, request.repository, request.setupToken); + const remoteSecrets = new Set(repositorySecretNames); + if (remoteConfiguration) { + for (const secret of remoteConfiguration.organizationSecrets) + remoteSecrets.add(secret); + } const requirements = (0, setup_configuration_policy_1.buildSetupCredentialRequirements)(request.configuration); const remoteHealth = this.remoteHealth ? await this.remoteHealth.validateExisting(request.owner, request.repository, request.setupToken, request.configuration.repository.mainBranch, requirements.filter(requirement => remoteSecrets.has(requirement.name))) @@ -59856,10 +60131,13 @@ class SetupCredentialsUseCase { } if (!this.secrets) throw new application_error_1.ApplicationError('Repository Secret provisioning is not available in this installation.', 'configuration'); - const existingSecretNames = await this.secrets.list(request.owner, request.repository, request.setupToken); + const existingSecretNames = request.remoteConfiguration?.repositorySecrets + ? [...request.remoteConfiguration.repositorySecrets] + : await this.secrets.list(request.owner, request.repository, request.setupToken); + const existingOrganizationSecretNames = request.remoteConfiguration?.organizationSecrets ?? []; const requirements = request.requirements.filter(requirement => requirement.name !== 'SETUP_PAT'); this.prompt.explainCredentialSeparation(requirements); - const existingRequirements = requirements.filter(requirement => existingSecretNames.includes(requirement.name)); + const existingRequirements = requirements.filter(requirement => existingSecretNames.includes(requirement.name) || existingOrganizationSecretNames.includes(requirement.name)); const remoteChecks = this.remoteHealth && existingRequirements.length > 0 ? await this.remoteHealth.validateExisting(request.owner, request.repository, request.setupToken, request.ref ?? 'master', existingRequirements) : undefined; @@ -59867,15 +60145,23 @@ class SetupCredentialsUseCase { const checks = [setupCheck]; const values = []; for (const requirement of requirements) { - const existing = existingSecretNames.includes(requirement.name); + const repositoryExisting = existingSecretNames.includes(requirement.name); + const organizationExisting = existingOrganizationSecretNames.includes(requirement.name); + const existing = repositoryExisting || organizationExisting; + const sourceScope = repositoryExisting + ? 'repository' + : organizationExisting + ? 'organization' + : undefined; if (existing) { const remoteCheck = remoteCheckByName.get(requirement.name) ?? { name: requirement.name, status: 'unverifiable', message: 'The remote health workflow is not available yet; GitHub does not reveal Secret values.', }; - checks.push(remoteCheck); - const decision = await this.prompt.chooseExistingCredential(requirement, remoteCheck); + const scopedCheck = { ...remoteCheck, sourceScope }; + checks.push(scopedCheck); + const decision = await this.prompt.chooseExistingCredential(requirement, scopedCheck); if (remoteCheck.status === 'invalid' && decision !== 'replace') { throw new application_error_1.ApplicationError(`${requirement.name} is invalid and must be replaced before setup can continue.`, 'authorization'); } @@ -59927,18 +60213,37 @@ exports.SetupWizardUseCase = void 0; const application_error_1 = __nccwpck_require__(75999); const setup_configuration_policy_1 = __nccwpck_require__(56637); class SetupWizardUseCase { - constructor(prompt) { + constructor(prompt, remoteConfigurationReader, storagePrompt) { this.prompt = prompt; + this.remoteConfigurationReader = remoteConfigurationReader; + this.storagePrompt = storagePrompt; } async collect(request = {}) { + this.lastRemoteConfiguration = undefined; const defaults = (0, setup_configuration_policy_1.mergeSetupConfiguration)((0, setup_configuration_policy_1.createDefaultSetupConfiguration)(), { ...request.overrides, ...(request.skipRepositoryVariables ? { manageRepositoryVariables: false } : {}), + ...(request.skipRepositorySecrets ? { manageRepositorySecrets: false } : {}), }); const collected = await this.prompt.collect(defaults); - const configuration = request.skipRepositoryVariables - ? { ...collected, manageRepositoryVariables: false } - : collected; + let configuration = { + ...collected, + ...(request.skipRepositoryVariables ? { manageRepositoryVariables: false } : {}), + ...(request.skipRepositorySecrets ? { manageRepositorySecrets: false } : {}), + }; + if (request.remoteTarget && this.remoteConfigurationReader && this.storagePrompt) { + const remote = await this.remoteConfigurationReader.inspect(request.remoteTarget.owner, request.remoteTarget.repository, request.remoteTarget.token); + this.lastRemoteConfiguration = remote; + const storage = await this.storagePrompt.chooseStorage((0, setup_configuration_policy_1.getSetupStorageConfiguration)(configuration), remote, (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(configuration), (0, setup_configuration_policy_1.buildSetupCredentialRequirements)(configuration), { + secrets: configuration.manageRepositorySecrets, + variables: configuration.manageRepositoryVariables, + }); + configuration = { ...configuration, storage }; + const remoteErrors = (0, setup_configuration_policy_1.validateSetupStorageAgainstRemote)(configuration, remote); + if (remoteErrors.length > 0) { + throw new application_error_1.ApplicationError(`Invalid remote storage configuration:\n${remoteErrors.map(error => `- ${error}`).join('\n')}`, 'authorization'); + } + } const validationErrors = (0, setup_configuration_policy_1.validateSetupConfiguration)(configuration); if (validationErrors.length > 0) { throw new application_error_1.ApplicationError(`Invalid setup configuration:\n${validationErrors.map(error => `- ${error}`).join('\n')}`, 'validation'); @@ -59952,6 +60257,9 @@ class SetupWizardUseCase { plan(configuration) { return (0, setup_configuration_policy_1.buildSetupPlan)(configuration); } + remoteConfiguration() { + return this.lastRemoteConfiguration; + } close() { this.prompt.close(); } @@ -66883,6 +67191,12 @@ function registerSetupCommand(program) { .option('--dry-run', 'Show the setup plan without changing files or GitHub', false) .option('--skip-variables', 'Do not create or update GitHub Repository Variables', false) .option('--skip-secrets', 'Do not validate or create/update GitHub Repository Secrets', false) + .option('--variables-scope ', 'Default Variable scope (repository|organization)') + .option('--secrets-scope ', 'Default Secret scope (repository|organization)') + .option('--variables-visibility ', 'Organization Variable visibility (selected|private|all)') + .option('--secrets-visibility ', 'Organization Secret visibility (selected|private|all)') + .option('--variable-scope ', 'Per-variable scope override; repeat as needed', collectScope, {}) + .option('--secret-scope ', 'Per-secret scope override; repeat as needed', collectScope, {}) .option('--update-workflows', 'Allow setup-managed workflows already in the repository to be updated', false) .option('--workflow-pat ', 'Workflow PAT for the bot account (prefer the hidden interactive prompt)') .option('--secret ', 'Secret value for non-interactive setup; repeat for each API key', collectSecret, {}) @@ -66925,11 +67239,16 @@ function registerSetupCommand(program) { return; } (0, logger_1.logInfo)(options.dryRun ? '🧭 Building a dry-run setup plan...' : '🧭 Building your setup plan...'); - const wizard = new setup_1.SetupWizardUseCase(prompt); + const remoteConfigurationReader = typeof setup_credentials_composition_root_1.createSetupRemoteConfigurationReadPort === 'function' + ? (0, setup_credentials_composition_root_1.createSetupRemoteConfigurationReadPort)() + : undefined; + const wizard = new setup_1.SetupWizardUseCase(prompt, remoteConfigurationReader, prompt); const overrides = loadSetupOverrides(options); const configuration = await wizard.collect({ overrides, skipRepositoryVariables: Boolean(options.skipVariables), + skipRepositorySecrets: Boolean(options.skipSecrets), + ...(token ? { remoteTarget: { owner: gitInfo.owner, repository: gitInfo.repo, token } } : {}), }); if (!configuration) { (0, logger_1.logInfo)('⏭️ Setup cancelled. No changes were applied.'); @@ -66951,9 +67270,10 @@ function registerSetupCommand(program) { requirements: (0, setup_configuration_policy_1.buildSetupCredentialRequirements)(configuration), manageSecrets: !options.skipSecrets && configuration.manageRepositorySecrets, ref: configuration.repository.mainBranch, + remoteConfiguration: wizard.remoteConfiguration(), }); (0, logger_1.logInfo)('⚙️ Applying the approved setup plan...'); - const params = (0, setup_policy_1.buildSetupParams)(options, gitInfo, token ?? '', configuration, credentials.collection, approvedWorkflowFiles); + const params = (0, setup_policy_1.buildSetupParams)(options, gitInfo, token ?? '', configuration, credentials.collection, approvedWorkflowFiles, wizard.remoteConfiguration()); if (!params) return; await (0, local_action_1.runLocalAction)(params); @@ -66998,6 +67318,23 @@ function loadSetupOverrides(options) { fromFlags.features = Object.fromEntries(Object.keys(setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, requested.includes(feature)])); } } + const storage = {}; + if (options.variablesScope || options.variablesVisibility || Object.keys(options.variableScope ?? {}).length > 0) { + storage.variables = { + ...(options.variablesScope ? { defaultScope: parseScope(options.variablesScope, '--variables-scope') } : {}), + ...(options.variablesVisibility ? { organizationVisibility: parseVisibility(options.variablesVisibility, '--variables-visibility') } : {}), + ...(Object.keys(options.variableScope ?? {}).length > 0 ? { overrides: options.variableScope } : {}), + }; + } + if (options.secretsScope || options.secretsVisibility || Object.keys(options.secretScope ?? {}).length > 0) { + storage.secrets = { + ...(options.secretsScope ? { defaultScope: parseScope(options.secretsScope, '--secrets-scope') } : {}), + ...(options.secretsVisibility ? { organizationVisibility: parseVisibility(options.secretsVisibility, '--secrets-visibility') } : {}), + ...(Object.keys(options.secretScope ?? {}).length > 0 ? { overrides: options.secretScope } : {}), + }; + } + if (Object.keys(storage).length > 0) + fromFlags.storage = storage; return mergeSetupOverrides(fromFile, fromFlags); } function mergeSetupOverrides(fileOverrides, flagOverrides) { @@ -67009,8 +67346,37 @@ function mergeSetupOverrides(fileOverrides, flagOverrides) { repository: { ...fileOverrides.repository, ...flagOverrides.repository }, ai: { ...fileOverrides.ai, ...flagOverrides.ai }, projects: { ...fileOverrides.projects, ...flagOverrides.projects }, + storage: { + ...fileOverrides.storage, + ...flagOverrides.storage, + secrets: { ...fileOverrides.storage?.secrets, ...flagOverrides.storage?.secrets, overrides: { ...fileOverrides.storage?.secrets?.overrides, ...flagOverrides.storage?.secrets?.overrides } }, + variables: { ...fileOverrides.storage?.variables, ...flagOverrides.storage?.variables, overrides: { ...fileOverrides.storage?.variables?.overrides, ...flagOverrides.storage?.variables?.overrides } }, + }, }; } +function collectScope(value, previous) { + const separator = value.indexOf('='); + if (separator <= 0) + throw new Error('Scope overrides must use NAME=repository or NAME=organization syntax.'); + const name = value.slice(0, separator).trim(); + const scope = value.slice(separator + 1).trim().toLowerCase(); + if (!/^[A-Z][A-Z0-9_]*$/.test(name) || !['repository', 'organization'].includes(scope)) { + throw new Error('Scope overrides must use an uppercase NAME and repository or organization scope.'); + } + return { ...previous, [name]: scope }; +} +function parseScope(value, flag) { + const normalized = value.trim().toLowerCase(); + if (normalized !== 'repository' && normalized !== 'organization') + throw new Error(`${flag} must be repository or organization.`); + return normalized; +} +function parseVisibility(value, flag) { + const normalized = value.trim().toLowerCase(); + if (!['all', 'private', 'selected'].includes(normalized)) + throw new Error(`${flag} must be selected, private, or all.`); + return normalized; +} /***/ }), @@ -67024,7 +67390,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildSetupParams = buildSetupParams; const constants_1 = __nccwpck_require__(15415); const setup_configuration_policy_1 = __nccwpck_require__(56637); -function buildSetupParams(options, gitInfo, token, configuration, credentials, approvedWorkflowFiles = []) { +function buildSetupParams(options, gitInfo, token, configuration, credentials, approvedWorkflowFiles = [], remoteConfiguration) { if ('error' in gitInfo) return undefined; return { @@ -67042,6 +67408,7 @@ function buildSetupParams(options, gitInfo, token, configuration, credentials, a ], ...(configuration ? { setupConfiguration: configuration } : {}), ...(credentials ? { setupCredentials: credentials } : {}), + ...(remoteConfiguration ? { setupRemoteConfiguration: remoteConfiguration } : {}), setupWorkflowUpdates: approvedWorkflowFiles, }; } @@ -67230,6 +67597,7 @@ const SETUP_OVERRIDE_KEYS = new Set([ 'manageRepositoryVariables', 'manageRepositorySecrets', 'actionInputs', + 'storage', ]); const AGENT_OVERRIDE_KEYS = new Set(['provider', 'modelProvider', 'model', 'effort']); const REPOSITORY_STRING_KEYS = new Set([ @@ -67257,6 +67625,8 @@ const PROJECT_KEYS = new Set([ 'issueInProgressColumn', 'pullRequestInProgressColumn', ]); +const STORAGE_KEYS = new Set(['secrets', 'variables']); +const STORAGE_POLICY_KEYS = new Set(['defaultScope', 'organizationVisibility', 'preserveExisting', 'overrides']); /** Loads a non-secret setup override file. JSON and YAML are supported. */ function loadSetupConfigurationOverrides(filePath) { const parsed = yaml.load((0, node_fs_1.readFileSync)(filePath, 'utf8')); @@ -67293,8 +67663,44 @@ function loadSetupConfigurationOverrides(filePath) { validateOptionalObject(raw.actionInputs, 'actionInputs'); if (raw.actionInputs !== undefined) validateStringValues(raw.actionInputs, 'actionInputs'); + validateStorage(raw.storage); return raw; } +function validateStorage(value) { + if (value === undefined) + return; + validateObject(value, 'storage'); + const storage = value; + validateObjectKeys(storage, STORAGE_KEYS, 'storage'); + for (const kind of STORAGE_KEYS) { + if (storage[kind] === undefined) + continue; + validateObject(storage[kind], `storage.${kind}`); + const policy = storage[kind]; + validateObjectKeys(policy, STORAGE_POLICY_KEYS, `storage.${kind}`); + if (policy.defaultScope !== undefined && !['repository', 'organization'].includes(String(policy.defaultScope))) { + throw new Error(`storage.${kind}.defaultScope must be repository or organization.`); + } + if (policy.organizationVisibility !== undefined && !['all', 'private', 'selected'].includes(String(policy.organizationVisibility))) { + throw new Error(`storage.${kind}.organizationVisibility must be all, private, or selected.`); + } + if (policy.preserveExisting !== undefined && typeof policy.preserveExisting !== 'boolean') { + throw new Error(`storage.${kind}.preserveExisting must be a boolean.`); + } + if (policy.overrides !== undefined) { + validateObject(policy.overrides, `storage.${kind}.overrides`); + validateStringValues(policy.overrides, `storage.${kind}.overrides`); + for (const [name, scope] of Object.entries(policy.overrides)) { + if (!/^[A-Z][A-Z0-9_]*$/.test(name)) { + throw new Error(`storage.${kind}.overrides names must be uppercase GitHub Actions names.`); + } + if (!['repository', 'organization'].includes(String(scope))) { + throw new Error(`storage.${kind}.overrides.${name} must be repository or organization.`); + } + } + } + } +} function validateSection(value, name, stringKeys, booleanKeys, numberKeys) { if (value === undefined) return; @@ -67338,19 +67744,24 @@ function validateBooleanProperty(value, key) { if (value[key] !== undefined && typeof value[key] !== 'boolean') throw new Error(`${key} must be a boolean.`); } -function containsCredentialMaterial(value) { +function containsCredentialMaterial(value, insideStorage = false) { if (typeof value === 'string') { return /^(?:github_pat_|gh[pso]_|ghu_|ghs_|sk-|AIza|xox[baprs]-)/i.test(value.trim()); } if (!value || typeof value !== 'object') return false; if (Array.isArray(value)) - return value.some(containsCredentialMaterial); + return value.some(item => containsCredentialMaterial(item, insideStorage)); return Object.entries(value).some(([key, item]) => { + if (insideStorage) + return false; + if (key === 'storage') + return containsCredentialMaterial(item, true); // Boolean configuration switches such as `manageRepositorySecrets` and // `features.credentialHealth` are not credential material. Only reject // credential-shaped properties when they actually carry a value. - const looksLikeCredentialProperty = /(?:password|secret|token|api[_-]?key|credential)/i.test(key); + const looksLikeCredentialProperty = /(?:password|secret|token|api[_-]?key|credential)/i.test(key) + && !['storage', 'secrets', 'variables'].includes(key.toLowerCase()); return (looksLikeCredentialProperty && item !== undefined && item !== null && typeof item !== 'boolean') || containsCredentialMaterial(item); }); @@ -67449,6 +67860,21 @@ class SetupPromptAdapter { defaults.manageRepositorySecrets = await this.askBoolean('Validate and provision the GitHub Secrets required by the selected workflows?', defaults.manageRepositorySecrets); return defaults; } + async chooseStorage(defaults, remote, variables, requirements, managed = { secrets: true, variables: true }) { + if (!this.readline) + return defaults; + console.log(color('\n5. Review GitHub Actions resource scopes\n', 36)); + console.log(renderBox(renderRemoteConfiguration(remote, variables, requirements), 'Existing GitHub Actions resources', 33)); + const secrets = managed.secrets + ? await this.chooseStoragePolicy('secrets', defaults.secrets, remote, requirements.map(requirement => requirement.name)) + : defaults.secrets; + const configuredVariables = variables.map(variable => variable.name); + const variableNames = configuredVariables.length > 0 ? configuredVariables : []; + const variablesPolicy = managed.variables + ? await this.chooseStoragePolicy('variables', defaults.variables, remote, variableNames) + : defaults.variables; + return { secrets, variables: variablesPolicy }; + } showPlan(plan) { const enabledFeatures = Object.entries(plan.configuration.features) .filter(([, enabled]) => enabled) @@ -67464,6 +67890,8 @@ class SetupPromptAdapter { ` Files selected: ${plan.selectedFiles.length}`, ` Variables to upsert: ${plan.configuration.manageRepositoryVariables ? plan.variables.length : 0}`, ` Secrets to validate/provision: ${plan.configuration.manageRepositorySecrets ? plan.credentialRequirements.length : 0}`, + ` Variable storage: ${plan.configuration.storage.variables.defaultScope} scope${plan.configuration.storage.variables.defaultScope === 'organization' ? ` (${plan.configuration.storage.variables.organizationVisibility})` : ''}`, + ` Secret storage: ${plan.configuration.storage.secrets.defaultScope} scope${plan.configuration.storage.secrets.defaultScope === 'organization' ? ` (${plan.configuration.storage.secrets.organizationVisibility})` : ''}`, ` Labels and issue types: always checked by Copilot setup`, ` Initial tag: ${plan.configuration.createInitialTag ? 'v1.0.0 when no version tag exists' : 'disabled'}`, '', color('Credential contract', 33), ` ${plan.requiredSecrets.join(', ')}`, @@ -67610,6 +68038,31 @@ class SetupPromptAdapter { console.log(color('Please select one of the listed options.', 33)); } } + async chooseStoragePolicy(kind, defaults, remote, names) { + const label = kind === 'secrets' ? 'Secrets' : 'Variables'; + const defaultScope = await this.askChoice(`Where should new GitHub Actions ${label} be stored?`, ['repository', 'organization'], defaults.defaultScope); + const organizationVisibility = (defaultScope === 'organization' || Object.values(defaults.overrides).includes('organization')) + ? await this.askChoice(`How should organization ${label} be shared?`, ['selected', 'private', 'all'], defaults.organizationVisibility) + : defaults.organizationVisibility; + const preserveExisting = await this.askBoolean(`Preserve existing effective ${label} instead of creating a shadowing override?`, defaults.preserveExisting); + const organizationNames = kind === 'secrets' + ? remote.organizationSecrets + : remote.organizationVariables.map(variable => variable.name); + const repositoryNames = kind === 'secrets' + ? remote.repositorySecrets + : remote.repositoryVariables.map(variable => variable.name); + const inherited = names.filter(name => organizationNames.includes(name) && !repositoryNames.includes(name)); + let overrides = { ...defaults.overrides }; + if (inherited.length > 0 && defaultScope === 'repository') { + const overrideInput = await this.askText(`Organization ${label} available to this repository: ${inherited.join(', ')}. Repository override names (comma-separated, empty to inherit all)`, ''); + const requested = new Set(overrideInput.split(',').map(name => name.trim()).filter(Boolean)); + overrides = { + ...overrides, + ...Object.fromEntries(inherited.filter(name => requested.has(name)).map(name => [name, 'repository'])), + }; + } + return { defaultScope, organizationVisibility, preserveExisting, overrides }; + } } exports.SetupPromptAdapter = SetupPromptAdapter; function statusIcon(status) { @@ -67645,6 +68098,22 @@ function renderBox(content, title, borderCode = 36) { bottom, ].join('\n'); } +function renderRemoteConfiguration(remote, variables, requirements) { + const lines = [ + `Target owner: ${remote.ownerType}; repository visibility: ${remote.repositoryVisibility}; repository ID: ${remote.repositoryId ?? 'unknown'}`, + `Repository Secrets: ${remote.repositorySecrets.length > 0 ? remote.repositorySecrets.join(', ') : '(none detected)'}`, + `Organization Secrets available here: ${remote.organizationSecrets.length > 0 ? remote.organizationSecrets.join(', ') : '(none detected)'}`, + `Repository Variables: ${remote.repositoryVariables.length > 0 ? remote.repositoryVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, + `Organization Variables available here: ${remote.organizationVariables.length > 0 ? remote.organizationVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, + `Required Secrets: ${requirements.map(requirement => requirement.name).join(', ')}`, + `Required Variables: ${variables.map(variable => variable.name).join(', ')}`, + remote.organizationAccess === 'available' + ? 'Organization resources can be inspected for this repository.' + : `Organization resource inspection: ${remote.organizationAccess}.`, + 'Repository-level resources take precedence over organization-level resources. Secret values are never displayed.', + ]; + return lines.join('\n'); +} function stripAnsi(value) { return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''); } @@ -73845,13 +74314,44 @@ class RepositoryVariablesRepository { const client = this.githubClient.getClient(token); if (!client.rest.secrets) throw new Error('GitHub repository Secret API is unavailable.'); - const response = await client.rest.secrets.listRepoSecrets({ owner, repo: repository, per_page: 100 }); - return response.data.secrets.map(secret => secret.name); + const secrets = await listCollection(client, client.rest.secrets.listRepoSecrets, { owner, repo: repository, per_page: 100 }, 'secrets'); + return secrets.map(secret => secret.name); } async listVariables(owner, repository, token) { const client = this.githubClient.getClient(token); - const response = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); - return response.data.variables.map(variable => ({ name: variable.name, value: variable.value })); + const variables = await listCollection(client, client.rest.actions.listRepoVariables, { owner, repo: repository, per_page: 100 }, 'variables'); + return variables.map(variable => ({ name: variable.name, ...(variable.value !== undefined ? { value: variable.value } : {}) })); + } + async inspect(owner, repository, token) { + const client = this.githubClient.getClient(token); + if (!client.rest.repos?.get) + throw new Error('GitHub repository metadata API is unavailable.'); + const repositoryResponse = await client.rest.repos.get({ owner, repo: repository }); + const metadata = repositoryResponse.data; + const ownerType = normalizeOwnerType(metadata.owner?.type); + const repositoryVisibility = normalizeRepositoryVisibility(metadata.visibility); + const repositorySecrets = client.rest.secrets + ? await this.list(owner, repository, token) + : []; + const repositoryVariables = (await this.listVariables(owner, repository, token)) + .filter((variable) => variable.value !== undefined) + .map(variable => ({ name: variable.name, value: variable.value })); + const organizationSecretsResult = await this.listOrganizationSecrets(client, owner, repository, ownerType); + const organizationVariablesResult = await this.listOrganizationVariables(client, owner, repository, ownerType); + return { + ownerType, + repositoryId: metadata.id, + repositoryVisibility, + repositorySecrets, + organizationSecrets: organizationSecretsResult.resources.map(resource => resource.name), + repositoryVariables, + organizationVariables: organizationVariablesResult.resources + .filter((resource) => resource.value !== undefined) + .map(resource => ({ name: resource.name, value: resource.value })), + organizationAccess: combineOrganizationAccess(organizationSecretsResult.access, organizationVariablesResult.access), + organizationSecretsAccess: organizationSecretsResult.access, + organizationVariablesAccess: organizationVariablesResult.access, + }; } async upsertSecrets(owner, repository, token, credentials) { const client = this.githubClient.getClient(token); @@ -73883,14 +74383,59 @@ class RepositoryVariablesRepository { } return { created, updated, skipped, errors }; } + async upsertScopedSecrets(owner, repository, token, target, credentials) { + if (target.scope === 'repository') + return this.upsertSecrets(owner, repository, token, credentials); + const client = this.githubClient.getClient(token); + const secrets = client.rest.secrets; + if (!secrets?.getOrgPublicKey || !secrets.createOrUpdateOrgSecret || !secrets.listOrgSecrets) { + throw new Error('GitHub organization Secret API is unavailable or the setup PAT lacks organization Secret permissions.'); + } + if (target.organizationVisibility === 'selected' && target.repositoryId === undefined) { + throw new Error('The repository ID is required for selected organization Secret access.'); + } + const existing = new Map((await listCollection(client, secrets.listOrgSecrets, { org: owner, per_page: 30 }, 'secrets')) + .map(secret => [secret.name, secret])); + const publicKey = await secrets.getOrgPublicKey({ org: owner }); + let created = 0; + let updated = 0; + const errors = []; + for (const credential of credentials) { + try { + const current = existing.get(credential.name); + const visibility = current?.visibility ?? target.organizationVisibility; + await secrets.createOrUpdateOrgSecret({ + org: owner, + secret_name: credential.name, + encrypted_value: encryptSecret(credential.value, publicKey.data.key), + key_id: publicKey.data.key_id, + visibility, + ...(visibility === 'selected' && target.repositoryId !== undefined && !current + ? { selected_repository_ids: [target.repositoryId] } + : {}), + }); + if (visibility === 'selected' && target.repositoryId !== undefined && secrets.addSelectedRepoToOrgSecret) { + await secrets.addSelectedRepoToOrgSecret({ org: owner, secret_name: credential.name, repository_id: target.repositoryId }); + } + if (current) + updated += 1; + else + created += 1; + } + catch (error) { + errors.push(`Error configuring organization Secret ${credential.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, skipped: 0, errors }; + } /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ async upsert(owner, repository, token, variables) { return this.upsertVariables(owner, repository, token, variables); } async upsertVariables(owner, repository, token, variables) { const client = this.githubClient.getClient(token); - const existing = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); - const existingValues = new Map(existing.data.variables.map(variable => [variable.name, variable.value])); + const existingVariables = await listCollection(client, client.rest.actions.listRepoVariables, { owner, repo: repository, per_page: 100 }, 'variables'); + const existingValues = new Map(existingVariables.map(variable => [variable.name, variable.value])); let created = 0; let updated = 0; const errors = []; @@ -73913,8 +74458,98 @@ class RepositoryVariablesRepository { } return { created, updated, errors }; } + async upsertScopedVariables(owner, repository, token, target, variables) { + if (target.scope === 'repository') + return this.upsert(owner, repository, token, variables); + const client = this.githubClient.getClient(token); + const actions = client.rest.actions; + if (!actions.listOrgVariables || !actions.createOrUpdateOrgVariable) { + throw new Error('GitHub organization Variable API is unavailable or the setup PAT lacks organization Variable permissions.'); + } + if (target.organizationVisibility === 'selected' && target.repositoryId === undefined) { + throw new Error('The repository ID is required for selected organization Variable access.'); + } + const existing = new Map((await listCollection(client, actions.listOrgVariables, { org: owner, per_page: 30 }, 'variables')) + .map(variable => [variable.name, variable])); + let created = 0; + let updated = 0; + const errors = []; + for (const variable of variables) { + try { + const current = existing.get(variable.name); + const visibility = current?.visibility ?? target.organizationVisibility; + await actions.createOrUpdateOrgVariable({ + org: owner, + name: variable.name, + value: variable.value, + visibility, + ...(visibility === 'selected' && target.repositoryId !== undefined && !current + ? { selected_repository_ids: [target.repositoryId] } + : {}), + }); + if (visibility === 'selected' && target.repositoryId !== undefined && actions.addSelectedRepoToOrgVariable) { + await actions.addSelectedRepoToOrgVariable({ org: owner, name: variable.name, repository_id: target.repositoryId }); + } + if (current) + updated += 1; + else + created += 1; + } + catch (error) { + errors.push(`Error configuring organization Variable ${variable.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, errors }; + } + async listOrganizationSecrets(client, owner, repository, ownerType) { + if (ownerType !== 'Organization') + return { resources: [], access: 'not_applicable' }; + const list = client.rest.secrets?.listRepoOrganizationSecrets; + if (!list) + return { resources: [], access: 'unknown' }; + try { + return { resources: await listCollection(client, list, { owner, repo: repository, per_page: 30 }, 'secrets'), access: 'available' }; + } + catch { + return { resources: [], access: 'unavailable' }; + } + } + async listOrganizationVariables(client, owner, repository, ownerType) { + if (ownerType !== 'Organization') + return { resources: [], access: 'not_applicable' }; + const list = client.rest.actions.listRepoOrganizationVariables; + if (!list) + return { resources: [], access: 'unknown' }; + try { + return { resources: await listCollection(client, list, { owner, repo: repository, per_page: 30 }, 'variables'), access: 'available' }; + } + catch { + return { resources: [], access: 'unavailable' }; + } + } } exports.RepositoryVariablesRepository = RepositoryVariablesRepository; +async function listCollection(client, method, parameters, key) { + if (client.paginate) + return client.paginate(method, parameters); + const response = await method(parameters); + return Array.isArray(response.data) ? response.data : response.data[key] ?? []; +} +function normalizeOwnerType(value) { + return value === 'Organization' ? 'Organization' : value === 'User' ? 'User' : 'Unknown'; +} +function normalizeRepositoryVisibility(value) { + return value === 'public' || value === 'private' || value === 'internal' ? value : 'unknown'; +} +function combineOrganizationAccess(secrets, variables) { + if (secrets === 'not_applicable' && variables === 'not_applicable') + return 'not_applicable'; + if (secrets === 'available' || variables === 'available') + return 'available'; + if (secrets === 'unavailable' || variables === 'unavailable') + return 'unavailable'; + return 'unknown'; +} /** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ function encryptSecret(value, base64PublicKey) { const publicKey = Buffer.from(base64PublicKey, 'base64'); @@ -75360,7 +75995,7 @@ const github_identity_client_factory_2 = __nccwpck_require__(93081); function createInitialSetupCompositionRoot() { const labelProvisioning = new issue_label_provisioning_repository_1.IssueLabelProvisioningRepository((0, github_issue_client_factory_1.createIssueLabelProvisioningClient)()); const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_2.createRepositoryVariablesClient)()); - return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter(), repositoryConfiguration, repositoryConfiguration); + return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter(), repositoryConfiguration, repositoryConfiguration, repositoryConfiguration); } @@ -75827,6 +76462,7 @@ function createPullRequestUseCaseCompositionRoot() { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createSetupCredentialsUseCase = createSetupCredentialsUseCase; +exports.createSetupRemoteConfigurationReadPort = createSetupRemoteConfigurationReadPort; const setup_credentials_use_case_1 = __nccwpck_require__(67438); const setup_credential_validation_adapter_1 = __nccwpck_require__(47020); const repository_variables_repository_1 = __nccwpck_require__(28493); @@ -75837,6 +76473,9 @@ function createSetupCredentialsUseCase(prompt) { const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_1.createRepositoryVariablesClient)()); return new setup_credentials_use_case_1.SetupCredentialsUseCase(prompt, new setup_credential_validation_adapter_1.SetupCredentialValidationAdapter(), repositoryConfiguration, new setup_remote_credential_health_adapter_1.SetupRemoteCredentialHealthAdapter(new octokit_credential_health_adapter_1.OctokitCredentialHealthClientAdapter(), { bootstrapWhenMissing: true })); } +function createSetupRemoteConfigurationReadPort() { + return new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_1.createRepositoryVariablesClient)()); +} /***/ }), @@ -75857,7 +76496,7 @@ const setup_remote_credential_health_adapter_1 = __nccwpck_require__(1489); const octokit_credential_health_adapter_1 = __nccwpck_require__(41760); function createSetupDoctorUseCase(output) { const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_1.createRepositoryVariablesClient)()); - return new doctor_use_case_1.SetupDoctorUseCase(new setup_credential_validation_adapter_1.SetupCredentialValidationAdapter(), repositoryConfiguration, repositoryConfiguration, new setup_workspace_adapter_1.SetupWorkspaceAdapter(), output, new setup_remote_credential_health_adapter_1.SetupRemoteCredentialHealthAdapter(new octokit_credential_health_adapter_1.OctokitCredentialHealthClientAdapter())); + return new doctor_use_case_1.SetupDoctorUseCase(new setup_credential_validation_adapter_1.SetupCredentialValidationAdapter(), repositoryConfiguration, repositoryConfiguration, new setup_workspace_adapter_1.SetupWorkspaceAdapter(), output, new setup_remote_credential_health_adapter_1.SetupRemoteCredentialHealthAdapter(new octokit_credential_health_adapter_1.OctokitCredentialHealthClientAdapter()), repositoryConfiguration); } diff --git a/build/cli/src/application/policies/setup_configuration_policy.d.ts b/build/cli/src/application/policies/setup_configuration_policy.d.ts index ade1bcf4..67ef0424 100644 --- a/build/cli/src/application/policies/setup_configuration_policy.d.ts +++ b/build/cli/src/application/policies/setup_configuration_policy.d.ts @@ -1,7 +1,8 @@ import type { AgentTask } from '../../domain/agent'; -import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement } from '../../domain/setup'; +import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement, SetupResourceScope, SetupResourceStoragePolicy, SetupStorageConfiguration, SetupRemoteConfiguration, SetupResourceTarget } from '../../domain/setup'; export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; +export declare function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration; export declare function createDefaultSetupConfiguration(): SetupConfiguration; export type SetupConfigurationOverrides = { features?: Partial; @@ -13,6 +14,10 @@ export type SetupConfigurationOverrides = { manageRepositoryVariables?: boolean; manageRepositorySecrets?: boolean; actionInputs?: Record; + storage?: { + secrets?: Partial; + variables?: Partial; + }; }; export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; @@ -21,3 +26,16 @@ export declare function buildSetupPlan(configuration: SetupConfiguration): Setup export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; +export declare function resolveSetupResourceScope(policy: SetupResourceStoragePolicy, name: string): SetupResourceScope; +export type SetupResourceKind = 'secret' | 'variable'; +export declare function getSetupResourceStoragePolicy(configuration: SetupConfiguration, kind: SetupResourceKind): SetupResourceStoragePolicy; +export declare function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration; +export declare function resolveSetupResourceTarget(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): SetupResourceTarget; +export declare function setupResourceExists(remote: SetupRemoteConfiguration | undefined, kind: SetupResourceKind, name: string): { + repository: boolean; + organization: boolean; + effective?: SetupResourceScope; +}; +export declare function shouldUpsertSetupResource(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): boolean; +export declare function validateSetupStorageAgainstRemote(configuration: SetupConfiguration, remote: SetupRemoteConfiguration): string[]; +export declare function usesOrganizationStorage(configuration: SetupConfiguration): boolean; diff --git a/build/cli/src/application/ports/setup_wizard_ports.d.ts b/build/cli/src/application/ports/setup_wizard_ports.d.ts index 3ede0673..081ad35b 100644 --- a/build/cli/src/application/ports/setup_wizard_ports.d.ts +++ b/build/cli/src/application/ports/setup_wizard_ports.d.ts @@ -1,10 +1,19 @@ -import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, DoctorCheck } from '../../domain/setup'; +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, DoctorCheck, SetupCredentialRequirement, SetupResourceTarget, SetupRemoteConfiguration, SetupStorageConfiguration, SetupVariable } from '../../domain/setup'; export interface SetupPromptPort { collect(defaults: SetupConfiguration): Promise; showPlan(plan: SetupPlan): void; confirm(plan: SetupPlan): Promise; close(): void; } +export interface SetupStoragePromptPort { + chooseStorage(defaults: SetupStorageConfiguration, remote: SetupRemoteConfiguration, variables: readonly SetupVariable[], requirements: readonly SetupCredentialRequirement[], managed?: { + secrets: boolean; + variables: boolean; + }): Promise; +} +export interface SetupRemoteConfigurationReadPort { + inspect(owner: string, repository: string, token: string): Promise; +} export interface SetupCredentialPromptPort { requestSetupPat(): Promise; explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; @@ -21,6 +30,12 @@ export interface SetupRepositorySecretsPort { skipped: number; errors: string[]; }>; + upsertScopedSecrets?(owner: string, repository: string, token: string, target: SetupResourceTarget, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; } export interface SetupRepositoryConfigurationReadPort { listVariables(owner: string, repository: string, token: string): Promise; + upsertScopedVariables?(owner: string, repository: string, token: string, target: SetupResourceTarget, variables: readonly { + name: string; + value: string; + }[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; } diff --git a/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts b/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts index aab353e2..ba6bb3d7 100644 --- a/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts +++ b/build/cli/src/application/usecases/actions/initial_setup_use_case.d.ts @@ -6,7 +6,7 @@ import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { ParamUseCase } from '../base/param_usecase'; -import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; /** Application boundary for provisioning a repository for Copilot automation. */ export declare class InitialSetupUseCase implements ParamUseCase { private readonly authenticatedUserPort; @@ -18,7 +18,8 @@ export declare class InitialSetupUseCase implements ParamUseCase; } diff --git a/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts b/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts index 5bc88b49..94717181 100644 --- a/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts +++ b/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts @@ -5,7 +5,7 @@ import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports/repository_release_ports'; import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; -import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; export interface InitialSetupWorkflowDependencies { authenticatedUserPort: AuthenticatedUserPort; initialLabelProvisioningPort: InitialLabelProvisioningPort; @@ -16,6 +16,7 @@ export interface InitialSetupWorkflowDependencies { setupWorkspacePort: SetupWorkspacePort; setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; setupRepositorySecretsPort?: SetupRepositorySecretsPort; + setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; } /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ export declare function runInitialSetupWorkflow(param: Execution, dependencies: InitialSetupWorkflowDependencies): Promise; diff --git a/build/cli/src/application/usecases/setup/doctor_use_case.d.ts b/build/cli/src/application/usecases/setup/doctor_use_case.d.ts index 16692f6f..83eb717f 100644 --- a/build/cli/src/application/usecases/setup/doctor_use_case.d.ts +++ b/build/cli/src/application/usecases/setup/doctor_use_case.d.ts @@ -1,5 +1,5 @@ import type { SetupConfiguration } from '../../../domain/setup'; -import type { DoctorOutputPort, SetupCredentialValidationPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +import type { DoctorOutputPort, SetupCredentialValidationPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRemoteConfigurationReadPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; export interface DoctorRequest { owner: string; @@ -14,6 +14,7 @@ export declare class SetupDoctorUseCase { private readonly workspace; private readonly output; private readonly remoteHealth?; - constructor(validation: SetupCredentialValidationPort, secrets: SetupRepositorySecretsPort, variables: SetupRepositoryConfigurationReadPort, workspace: SetupWorkspacePort, output: DoctorOutputPort, remoteHealth?: SetupRemoteCredentialHealthPort | undefined); + private readonly remoteConfigurationReader?; + constructor(validation: SetupCredentialValidationPort, secrets: SetupRepositorySecretsPort, variables: SetupRepositoryConfigurationReadPort, workspace: SetupWorkspacePort, output: DoctorOutputPort, remoteHealth?: SetupRemoteCredentialHealthPort | undefined, remoteConfigurationReader?: SetupRemoteConfigurationReadPort | undefined); execute(request: DoctorRequest): Promise; } diff --git a/build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts b/build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts index a6a3295f..d8e4c4f7 100644 --- a/build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts +++ b/build/cli/src/application/usecases/setup/setup_credentials_use_case.d.ts @@ -1,5 +1,6 @@ import type { SetupCredentialCheck, SetupCredentialCollection, SetupCredentialRequirement } from '../../../domain/setup'; import type { SetupCredentialPromptPort, SetupCredentialValidationPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +import type { SetupRemoteConfiguration } from '../../../domain/setup'; export interface SetupCredentialsRequest { owner: string; repository: string; @@ -7,6 +8,7 @@ export interface SetupCredentialsRequest { requirements: readonly SetupCredentialRequirement[]; manageSecrets: boolean; ref?: string; + remoteConfiguration?: SetupRemoteConfiguration; } export interface SetupCredentialsResult { collection: SetupCredentialCollection; diff --git a/build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts b/build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts index ad5dfab9..a8cc5c19 100644 --- a/build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts +++ b/build/cli/src/application/usecases/setup/setup_wizard_use_case.d.ts @@ -1,14 +1,24 @@ -import type { SetupPromptPort } from '../../ports/setup_wizard_ports'; -import type { SetupConfiguration, SetupPlan } from '../../../domain/setup'; +import type { SetupPromptPort, SetupRemoteConfigurationReadPort, SetupStoragePromptPort } from '../../ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan, SetupRemoteConfiguration } from '../../../domain/setup'; import { type SetupConfigurationOverrides } from '../../policies/setup_configuration_policy'; export interface SetupWizardRequest { overrides?: SetupConfigurationOverrides; skipRepositoryVariables?: boolean; + skipRepositorySecrets?: boolean; + remoteTarget?: { + owner: string; + repository: string; + token: string; + }; } export declare class SetupWizardUseCase { private readonly prompt; - constructor(prompt: SetupPromptPort); + private readonly remoteConfigurationReader?; + private readonly storagePrompt?; + private lastRemoteConfiguration; + constructor(prompt: SetupPromptPort, remoteConfigurationReader?: SetupRemoteConfigurationReadPort | undefined, storagePrompt?: SetupStoragePromptPort | undefined); collect(request?: SetupWizardRequest): Promise; plan(configuration: SetupConfiguration): SetupPlan; + remoteConfiguration(): SetupRemoteConfiguration | undefined; close(): void; } diff --git a/build/cli/src/cli/commands/setup_policy.d.ts b/build/cli/src/cli/commands/setup_policy.d.ts index 670911a0..42c06cb1 100644 --- a/build/cli/src/cli/commands/setup_policy.d.ts +++ b/build/cli/src/cli/commands/setup_policy.d.ts @@ -1,6 +1,6 @@ import type { GitInfo } from '../../cli_context'; -import type { SetupConfiguration, SetupCredentialCollection } from '../../domain/setup'; +import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration } from '../../domain/setup'; export interface SetupCommandOptions { debug?: boolean; } -export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string, configuration?: SetupConfiguration, credentials?: SetupCredentialCollection, approvedWorkflowFiles?: readonly string[]): Record | undefined; +export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string, configuration?: SetupConfiguration, credentials?: SetupCredentialCollection, approvedWorkflowFiles?: readonly string[], remoteConfiguration?: SetupRemoteConfiguration): Record | undefined; diff --git a/build/cli/src/cli/setup_prompt_adapter.d.ts b/build/cli/src/cli/setup_prompt_adapter.d.ts index 1ad2dd3d..3665e0e4 100644 --- a/build/cli/src/cli/setup_prompt_adapter.d.ts +++ b/build/cli/src/cli/setup_prompt_adapter.d.ts @@ -1,17 +1,21 @@ -import type { SetupCredentialPromptPort, SetupPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort } from '../application/ports/setup_wizard_ports'; -import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison } from '../domain/setup'; +import type { SetupCredentialPromptPort, SetupStoragePromptPort, SetupPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort } from '../application/ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, SetupStorageConfiguration, SetupRemoteConfiguration, SetupVariable } from '../domain/setup'; export interface SetupPromptAdapterOptions { interactive?: boolean; assumeYes?: boolean; credentialValues?: Record; } -export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { +export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupStoragePromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { private readonly interactive; private readonly assumeYes; private readonly readline; private readonly credentialValues; constructor(options?: SetupPromptAdapterOptions); collect(defaults: SetupConfiguration): Promise; + chooseStorage(defaults: SetupStorageConfiguration, remote: SetupRemoteConfiguration, variables: readonly SetupVariable[], requirements: readonly SetupCredentialRequirement[], managed?: { + secrets: boolean; + variables: boolean; + }): Promise; showPlan(plan: SetupPlan): void; confirm(plan: SetupPlan): Promise; requestSetupPat(): Promise; @@ -29,4 +33,5 @@ export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredent private askNumber; private askBoolean; private askChoice; + private chooseStoragePolicy; } diff --git a/build/cli/src/data/repository/repository_variables_repository.d.ts b/build/cli/src/data/repository/repository_variables_repository.d.ts index bd7ceeb5..6dd314fe 100644 --- a/build/cli/src/data/repository/repository_variables_repository.d.ts +++ b/build/cli/src/data/repository/repository_variables_repository.d.ts @@ -1,8 +1,8 @@ -import type { SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; -import type { SetupCredentialValue } from '../../domain/setup'; +import type { SetupRemoteConfigurationReadPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialValue, SetupRemoteConfiguration, SetupResourceTarget, SetupVariable } from '../../domain/setup'; import type { GithubClientPort } from '../../infrastructure/github/ports/github_client_provider_port'; import type { GithubRepositoryVariablesClient } from '../../infrastructure/github/ports/github_repository_variables_protocol'; -export declare class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort { +export declare class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort, SetupRemoteConfigurationReadPort { private readonly githubClient; constructor(githubClient: GithubClientPort); list(owner: string, repository: string, token: string): Promise; @@ -10,12 +10,19 @@ export declare class RepositoryVariablesRepository implements SetupRepositoryVar name: string; value?: string; }[]>; + inspect(owner: string, repository: string, token: string): Promise; upsertSecrets(owner: string, repository: string, token: string, credentials: readonly SetupCredentialValue[]): Promise<{ created: number; updated: number; skipped: number; errors: string[]; }>; + upsertScopedSecrets(owner: string, repository: string, token: string, target: SetupResourceTarget, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ upsert(owner: string, repository: string, token: string, variables: readonly { name: string; @@ -26,6 +33,13 @@ export declare class RepositoryVariablesRepository implements SetupRepositoryVar errors: string[]; }>; private upsertVariables; + upsertScopedVariables(owner: string, repository: string, token: string, target: SetupResourceTarget, variables: readonly SetupVariable[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; + private listOrganizationSecrets; + private listOrganizationVariables; } /** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ export declare function encryptSecret(value: string, base64PublicKey: string): string; diff --git a/build/cli/src/domain/setup.d.ts b/build/cli/src/domain/setup.d.ts index 668d18ad..12b58f46 100644 --- a/build/cli/src/domain/setup.d.ts +++ b/build/cli/src/domain/setup.d.ts @@ -60,6 +60,27 @@ export interface SetupConfiguration { manageRepositorySecrets: boolean; /** Extra non-secret action inputs accepted by config files for advanced use cases. */ actionInputs: Record; + /** Independent storage policies for non-sensitive variables and secrets. */ + storage: SetupStorageConfiguration; +} +export type SetupResourceScope = 'repository' | 'organization'; +export type SetupOrganizationVisibility = 'all' | 'private' | 'selected'; +export interface SetupResourceStoragePolicy { + defaultScope: SetupResourceScope; + organizationVisibility: SetupOrganizationVisibility; + /** Keep an already-effective resource instead of creating a shadowing override. */ + preserveExisting: boolean; + /** Per-resource exceptions for mixed repository/organization configurations. */ + overrides: Record; +} +export interface SetupResourceTarget { + scope: SetupResourceScope; + organizationVisibility: SetupOrganizationVisibility; + repositoryId?: number; +} +export interface SetupStorageConfiguration { + secrets: SetupResourceStoragePolicy; + variables: SetupResourceStoragePolicy; } export type SetupCredentialKind = 'workflowPat' | 'apiKey'; export type SetupCredentialStatus = 'valid' | 'invalid' | 'missing' | 'unverifiable' | 'not_required'; @@ -76,6 +97,7 @@ export interface SetupCredentialCheck { status: SetupCredentialStatus; message: string; account?: string; + sourceScope?: SetupResourceScope; } export interface SetupCredentialValue { name: string; @@ -86,6 +108,20 @@ export interface SetupCredentialCollection { workflowPat?: SetupCredentialValue; apiKeys: SetupCredentialValue[]; } +export type SetupOwnerType = 'User' | 'Organization' | 'Unknown'; +export type SetupRepositoryVisibility = 'public' | 'private' | 'internal' | 'unknown'; +export interface SetupRemoteConfiguration { + ownerType: SetupOwnerType; + repositoryId?: number; + repositoryVisibility: SetupRepositoryVisibility; + repositorySecrets: readonly string[]; + organizationSecrets: readonly string[]; + repositoryVariables: readonly SetupVariable[]; + organizationVariables: readonly SetupVariable[]; + organizationAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; + organizationSecretsAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; + organizationVariablesAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; +} export interface SetupWorkflowComparison { file: string; destination: string; diff --git a/build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts b/build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts index 0c0bf531..634eec14 100644 --- a/build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts +++ b/build/cli/src/infrastructure/composition/setup_credentials_composition_root.d.ts @@ -1,3 +1,4 @@ import { SetupCredentialsUseCase } from '../../application/usecases/setup/setup_credentials_use_case'; -import type { SetupCredentialPromptPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialPromptPort, SetupRemoteConfigurationReadPort } from '../../application/ports/setup_wizard_ports'; export declare function createSetupCredentialsUseCase(prompt: SetupCredentialPromptPort): SetupCredentialsUseCase; +export declare function createSetupRemoteConfigurationReadPort(): SetupRemoteConfigurationReadPort; diff --git a/build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts b/build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts index 37799fed..ed681400 100644 --- a/build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts +++ b/build/cli/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts @@ -2,8 +2,30 @@ export interface GithubRepositoryVariable { name: string; value?: string; } +export interface GithubOrganizationResource { + name: string; + value?: string; + visibility?: 'all' | 'private' | 'selected'; + selected_repositories_url?: string; +} +export interface GithubRepositoryMetadata { + id?: number; + visibility?: string; + owner?: { + type?: string; + }; +} +export interface GithubActionsPublicKey { + key_id: string; + key: string; +} export interface GithubRepositoryVariablesClient { rest: { + repos?: { + get(parameters: Record): Promise<{ + data: GithubRepositoryMetadata; + }>; + }; actions: { listRepoVariables(parameters: Record): Promise<{ data: { @@ -12,6 +34,18 @@ export interface GithubRepositoryVariablesClient { }>; createRepoVariable(parameters: Record): Promise; updateRepoVariable(parameters: Record): Promise; + listRepoOrganizationVariables?: (parameters: Record) => Promise<{ + data: { + variables: GithubOrganizationResource[]; + }; + }>; + listOrgVariables?: (parameters: Record) => Promise<{ + data: { + variables: GithubOrganizationResource[]; + }; + }>; + createOrUpdateOrgVariable?: (parameters: Record) => Promise; + addSelectedRepoToOrgVariable?: (parameters: Record) => Promise; }; secrets?: { listRepoSecrets(parameters: Record): Promise<{ @@ -20,14 +54,32 @@ export interface GithubRepositoryVariablesClient { }; }>; getRepoPublicKey(parameters: Record): Promise<{ + data: GithubActionsPublicKey; + }>; + createOrUpdateRepoSecret(parameters: Record): Promise; + listRepoOrganizationSecrets?: (parameters: Record) => Promise<{ data: { - key_id: string; - key: string; + secrets: GithubOrganizationResource[]; }; }>; - createOrUpdateRepoSecret(parameters: Record): Promise; + listOrgSecrets?: (parameters: Record) => Promise<{ + data: { + secrets: GithubOrganizationResource[]; + }; + }>; + getOrgPublicKey?: (parameters: Record) => Promise<{ + data: GithubActionsPublicKey; + }>; + createOrUpdateOrgSecret?: (parameters: Record) => Promise; + addSelectedRepoToOrgSecret?: (parameters: Record) => Promise; }; }; + paginate?: (method: (parameters: Record) => Promise<{ + data: T[] | { + variables?: T[]; + secrets?: T[]; + }; + }>, parameters: Record) => Promise; } export interface GithubRepositorySecret { name: string; diff --git a/build/github_action/index.js b/build/github_action/index.js index 5e01aef2..5cecd150 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -53514,6 +53514,7 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SETUP_FEATURE_DESCRIPTIONS = exports.SETUP_AGENT_TASKS = void 0; +exports.createDefaultSetupStorageConfiguration = createDefaultSetupStorageConfiguration; exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; exports.mergeSetupConfiguration = mergeSetupConfiguration; exports.validateSetupConfiguration = validateSetupConfiguration; @@ -53521,6 +53522,14 @@ exports.buildSetupPlan = buildSetupPlan; exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; exports.buildSetupActionInputs = buildSetupActionInputs; +exports.resolveSetupResourceScope = resolveSetupResourceScope; +exports.getSetupResourceStoragePolicy = getSetupResourceStoragePolicy; +exports.getSetupStorageConfiguration = getSetupStorageConfiguration; +exports.resolveSetupResourceTarget = resolveSetupResourceTarget; +exports.setupResourceExists = setupResourceExists; +exports.shouldUpsertSetupResource = shouldUpsertSetupResource; +exports.validateSetupStorageAgainstRemote = validateSetupStorageAgainstRemote; +exports.usesOrganizationStorage = usesOrganizationStorage; const agent_1 = __nccwpck_require__(89040); const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); const pull_request_description_1 = __nccwpck_require__(45315); @@ -53572,6 +53581,21 @@ const SECRET_BY_MODEL_PROVIDER = { google: 'GOOGLE_API_KEY', openrouter: 'OPENROUTER_API_KEY', }; +const RESOURCE_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/; +function defaultStoragePolicy() { + return { + defaultScope: 'repository', + organizationVisibility: 'selected', + preserveExisting: true, + overrides: {}, + }; +} +function createDefaultSetupStorageConfiguration() { + return { + secrets: defaultStoragePolicy(), + variables: defaultStoragePolicy(), + }; +} function createDefaultSetupConfiguration() { const defaultRole = () => ({ provider: agent_1.DEFAULT_AGENT_PROVIDER, @@ -53624,6 +53648,7 @@ function createDefaultSetupConfiguration() { manageRepositoryVariables: true, manageRepositorySecrets: true, actionInputs: {}, + storage: createDefaultSetupStorageConfiguration(), }; } function mergeSetupConfiguration(base, overrides = {}) { @@ -53642,6 +53667,10 @@ function mergeSetupConfiguration(base, overrides = {}) { manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, + storage: { + secrets: mergeStoragePolicy(base.storage?.secrets, overrides.storage?.secrets), + variables: mergeStoragePolicy(base.storage?.variables, overrides.storage?.variables), + }, }; } function validateSetupConfiguration(configuration) { @@ -53681,6 +53710,7 @@ function validateSetupConfiguration(configuration) { if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } + errors.push(...validateStorageConfiguration(configuration.storage)); for (const task of exports.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) @@ -53860,7 +53890,7 @@ function buildRequiredSetupSecrets(configuration) { function buildSetupWarnings(configuration) { const warnings = []; if (configuration.features.release !== false && configuration.features.hotfix !== false) { - warnings.push('Release and hotfix workflows require the repository secret PAT and a writable token.'); + warnings.push('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); } if (configuration.ai.provisioningMode === 'always') { warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); @@ -53871,8 +53901,128 @@ function buildSetupWarnings(configuration) { if (exports.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); } + if (usesOrganizationStorage(configuration)) { + warnings.push('Organization-level Secrets and Variables require organization permissions; selected access is the safest default and repository values take precedence.'); + } return warnings; } +function resolveSetupResourceScope(policy, name) { + return policy.overrides[name] ?? policy.defaultScope; +} +function getSetupResourceStoragePolicy(configuration, kind) { + return getSetupStorageConfiguration(configuration)[kind === 'secret' ? 'secrets' : 'variables']; +} +function getSetupStorageConfiguration(configuration) { + const fallback = createDefaultSetupStorageConfiguration(); + return { + secrets: mergeStoragePolicy(fallback.secrets, configuration.storage?.secrets), + variables: mergeStoragePolicy(fallback.variables, configuration.storage?.variables), + }; +} +function resolveSetupResourceTarget(configuration, kind, name, remote) { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + const existingScope = setupResourceExists(remote, kind, name).effective; + const scope = existingScope && policy.preserveExisting && !explicitOverride + ? existingScope + : resolveSetupResourceScope(policy, name); + return { + scope, + organizationVisibility: policy.organizationVisibility, + repositoryId: remote?.repositoryId, + }; +} +function setupResourceExists(remote, kind, name) { + if (!remote) + return { repository: false, organization: false }; + const repository = kind === 'secret' + ? remote.repositorySecrets.includes(name) + : remote.repositoryVariables.some(variable => variable.name === name); + const organizationAccess = kind === 'secret' + ? (remote.organizationSecretsAccess ?? remote.organizationAccess) + : (remote.organizationVariablesAccess ?? remote.organizationAccess); + const organization = organizationAccess === 'available' && (kind === 'secret' + ? remote.organizationSecrets.includes(name) + : remote.organizationVariables.some(variable => variable.name === name)); + return { + repository, + organization, + effective: repository ? 'repository' : organization ? 'organization' : undefined, + }; +} +function shouldUpsertSetupResource(configuration, kind, name, remote) { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const state = setupResourceExists(remote, kind, name); + if (!state.effective) + return true; + const requested = resolveSetupResourceScope(policy, name); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + return requested === state.effective || explicitOverride || !policy.preserveExisting; +} +function validateSetupStorageAgainstRemote(configuration, remote) { + const errors = []; + const policies = [ + ['secret', getSetupResourceStoragePolicy(configuration, 'secret'), configuration.manageRepositorySecrets], + ['variable', getSetupResourceStoragePolicy(configuration, 'variable'), configuration.manageRepositoryVariables], + ]; + for (const [kind, policy, managed] of policies) { + if (!managed) + continue; + const needsOrganization = policy.defaultScope === 'organization' + || Object.values(policy.overrides).includes('organization'); + if (!needsOrganization) + continue; + if (remote.ownerType !== 'Organization') { + errors.push(`Organization-level ${kind} storage is only available for organization-owned repositories.`); + continue; + } + const access = kind === 'secret' ? remote.organizationSecretsAccess : remote.organizationVariablesAccess; + if (access !== 'available') { + errors.push(`The setup PAT cannot inspect organization ${kind}s for this repository. Organization ${kind} permissions are required.`); + } + if (policy.organizationVisibility === 'selected' && remote.repositoryId === undefined) { + errors.push(`The repository ID is required for selected organization ${kind} access.`); + } + } + return errors; +} +function usesOrganizationStorage(configuration) { + const storage = getSetupStorageConfiguration(configuration); + return [storage.secrets, storage.variables].some(policy => policy.defaultScope === 'organization' || Object.values(policy.overrides).includes('organization')); +} +function mergeStoragePolicy(base, override) { + const fallback = base ?? defaultStoragePolicy(); + return { + ...fallback, + ...(override ?? {}), + overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, + }; +} +function validateStorageConfiguration(storage) { + // Setup files created before scoped storage was introduced remain valid and + // receive the repository-level defaults through getSetupStorageConfiguration. + if (!storage) + return []; + const errors = []; + for (const [kind, policy] of Object.entries(storage)) { + if (!policy || !['repository', 'organization'].includes(policy.defaultScope)) { + errors.push(`${kind} default scope must be repository or organization.`); + continue; + } + if (!['all', 'private', 'selected'].includes(policy.organizationVisibility)) { + errors.push(`${kind} organization visibility must be all, private, or selected.`); + } + if (typeof policy.preserveExisting !== 'boolean') + errors.push(`${kind} preserveExisting must be a boolean.`); + for (const [name, scope] of Object.entries(policy.overrides ?? {})) { + if (!RESOURCE_NAME_PATTERN.test(name)) + errors.push(`${kind} override name ${name} must be an uppercase GitHub Actions name.`); + if (!['repository', 'organization'].includes(scope)) + errors.push(`${kind} override ${name} must use repository or organization.`); + } + } + return errors; +} function unique(values) { return [...new Set(values.map(value => value.trim()).filter(Boolean))]; } @@ -54624,7 +54774,7 @@ exports.InitialSetupUseCase = void 0; const initial_setup_workflow_1 = __nccwpck_require__(18079); /** Application boundary for provisioning a repository for Copilot automation. */ class InitialSetupUseCase { - constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort) { + constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort, setupRemoteConfigurationReadPort) { this.authenticatedUserPort = authenticatedUserPort; this.initialLabelProvisioningPort = initialLabelProvisioningPort; this.issueTypeProvisioningPort = issueTypeProvisioningPort; @@ -54634,6 +54784,7 @@ class InitialSetupUseCase { this.setupWorkspacePort = setupWorkspacePort; this.setupRepositoryVariablesPort = setupRepositoryVariablesPort; this.setupRepositorySecretsPort = setupRepositorySecretsPort; + this.setupRemoteConfigurationReadPort = setupRemoteConfigurationReadPort; this.taskId = 'InitialSetupUseCase'; } async invoke(param) { @@ -54647,6 +54798,7 @@ class InitialSetupUseCase { setupWorkspacePort: this.setupWorkspacePort, setupRepositoryVariablesPort: this.setupRepositoryVariablesPort, setupRepositorySecretsPort: this.setupRepositorySecretsPort, + setupRemoteConfigurationReadPort: this.setupRemoteConfigurationReadPort, }); } } @@ -54698,7 +54850,8 @@ async function runInitialSetupWorkflow(param, dependencies) { return [buildResult(errors, steps)]; } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); - const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration); + const remoteConfiguration = await resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors); + const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration); if (secrets.step) steps.push(secrets.step); if (secrets.errors.length > 0) @@ -54720,7 +54873,7 @@ async function runInitialSetupWorkflow(param, dependencies) { else { steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration); + const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration); if (variables.step) steps.push(variables.step); if (variables.errors.length > 0) @@ -54812,16 +54965,18 @@ function getWorkflowUpdates(param) { const updates = param.inputs?.setupWorkflowUpdates; return Array.isArray(updates) ? updates.filter((file) => typeof file === 'string') : []; } -async function ensureRepositoryVariables(param, dependencies, setupConfiguration) { +async function ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration) { if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { return { errors: [] }; } try { - const result = await dependencies.setupRepositoryVariablesPort.upsert(param.owner, param.repo, param.tokens.token, (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration)); + const desired = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration); + const groups = groupResources(desired, 'variable', setupConfiguration, remoteConfiguration); + const result = await upsertVariableGroups(param, dependencies.setupRepositoryVariablesPort, groups); if (result.errors.length > 0) return { errors: result.errors }; return { - step: `✅ Repository Variables: ${result.created} created, ${result.updated} updated`, + step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, errors: [], }; } @@ -54831,7 +54986,7 @@ async function ensureRepositoryVariables(param, dependencies, setupConfiguration return { errors: [message] }; } } -async function ensureRepositorySecrets(param, dependencies, setupConfiguration) { +async function ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration) { if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { return { errors: [] }; } @@ -54846,11 +55001,12 @@ async function ensureRepositorySecrets(param, dependencies, setupConfiguration) if (values.length === 0) return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; try { - const result = await dependencies.setupRepositorySecretsPort.upsertSecrets(param.owner, param.repo, param.tokens.token, values); + const groups = groupResources(values, 'secret', setupConfiguration, remoteConfiguration); + const result = await upsertSecretGroups(param, dependencies.setupRepositorySecretsPort, groups); if (result.errors.length > 0) return { errors: result.errors }; return { - step: `✅ Repository Secrets: ${result.created} created, ${result.updated} updated; existing values kept when no replacement was selected.`, + step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, errors: [], }; } @@ -54860,6 +55016,77 @@ async function ensureRepositorySecrets(param, dependencies, setupConfiguration) return { errors: [message] }; } } +async function resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors) { + const provided = param.inputs?.setupRemoteConfiguration; + if (provided && typeof provided === 'object') + return provided; + if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) + return undefined; + try { + return await dependencies.setupRemoteConfigurationReadPort.inspect(param.owner, param.repo, param.tokens.token); + } + catch (error) { + const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + (0, logging_ports_1.logError)(message); + if ((0, setup_configuration_policy_1.usesOrganizationStorage)(setupConfiguration)) + errors.push(message); + return undefined; + } +} +function groupResources(resources, kind, configuration, remoteConfiguration) { + const groups = new Map(); + for (const resource of resources) { + // Secret values reach this workflow only after the user chose keep/replace. + // Variables, however, are always generated from the selected setup contract, + // so preserveExisting must be applied here to avoid shadowing inherited values. + if (kind === 'variable' && !(0, setup_configuration_policy_1.shouldUpsertSetupResource)(configuration, kind, resource.name, remoteConfiguration)) + continue; + const target = (0, setup_configuration_policy_1.resolveSetupResourceTarget)(configuration, kind, resource.name, remoteConfiguration); + const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; + const group = groups.get(key) ?? { target, resources: [] }; + group.resources.push(resource); + groups.set(key, group); + } + return [...groups.values()]; +} +async function upsertVariableGroups(param, port, groups) { + let created = 0; + let updated = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedVariables) { + errors.push('Organization Variable provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedVariables(param.owner, param.repo, param.tokens.token, group.target, group.resources) + : await port.upsert(param.owner, param.repo, param.tokens.token, group.resources); + created += result.created; + updated += result.updated; + errors.push(...result.errors); + } + return { created, updated, errors }; +} +async function upsertSecretGroups(param, port, groups) { + let created = 0; + let updated = 0; + let skipped = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { + errors.push('Organization Secret provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedSecrets(param.owner, param.repo, param.tokens.token, group.target, group.resources) + : await port.upsertSecrets(param.owner, param.repo, param.tokens.token, group.resources); + created += result.created; + updated += result.updated; + skipped += result.skipped; + errors.push(...result.errors); + } + return { created, updated, skipped, errors }; +} function getSetupCredentialCollection(param) { const credentials = param.inputs?.setupCredentials; if (!credentials || typeof credentials !== 'object') @@ -69107,13 +69334,44 @@ class RepositoryVariablesRepository { const client = this.githubClient.getClient(token); if (!client.rest.secrets) throw new Error('GitHub repository Secret API is unavailable.'); - const response = await client.rest.secrets.listRepoSecrets({ owner, repo: repository, per_page: 100 }); - return response.data.secrets.map(secret => secret.name); + const secrets = await listCollection(client, client.rest.secrets.listRepoSecrets, { owner, repo: repository, per_page: 100 }, 'secrets'); + return secrets.map(secret => secret.name); } async listVariables(owner, repository, token) { const client = this.githubClient.getClient(token); - const response = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); - return response.data.variables.map(variable => ({ name: variable.name, value: variable.value })); + const variables = await listCollection(client, client.rest.actions.listRepoVariables, { owner, repo: repository, per_page: 100 }, 'variables'); + return variables.map(variable => ({ name: variable.name, ...(variable.value !== undefined ? { value: variable.value } : {}) })); + } + async inspect(owner, repository, token) { + const client = this.githubClient.getClient(token); + if (!client.rest.repos?.get) + throw new Error('GitHub repository metadata API is unavailable.'); + const repositoryResponse = await client.rest.repos.get({ owner, repo: repository }); + const metadata = repositoryResponse.data; + const ownerType = normalizeOwnerType(metadata.owner?.type); + const repositoryVisibility = normalizeRepositoryVisibility(metadata.visibility); + const repositorySecrets = client.rest.secrets + ? await this.list(owner, repository, token) + : []; + const repositoryVariables = (await this.listVariables(owner, repository, token)) + .filter((variable) => variable.value !== undefined) + .map(variable => ({ name: variable.name, value: variable.value })); + const organizationSecretsResult = await this.listOrganizationSecrets(client, owner, repository, ownerType); + const organizationVariablesResult = await this.listOrganizationVariables(client, owner, repository, ownerType); + return { + ownerType, + repositoryId: metadata.id, + repositoryVisibility, + repositorySecrets, + organizationSecrets: organizationSecretsResult.resources.map(resource => resource.name), + repositoryVariables, + organizationVariables: organizationVariablesResult.resources + .filter((resource) => resource.value !== undefined) + .map(resource => ({ name: resource.name, value: resource.value })), + organizationAccess: combineOrganizationAccess(organizationSecretsResult.access, organizationVariablesResult.access), + organizationSecretsAccess: organizationSecretsResult.access, + organizationVariablesAccess: organizationVariablesResult.access, + }; } async upsertSecrets(owner, repository, token, credentials) { const client = this.githubClient.getClient(token); @@ -69145,14 +69403,59 @@ class RepositoryVariablesRepository { } return { created, updated, skipped, errors }; } + async upsertScopedSecrets(owner, repository, token, target, credentials) { + if (target.scope === 'repository') + return this.upsertSecrets(owner, repository, token, credentials); + const client = this.githubClient.getClient(token); + const secrets = client.rest.secrets; + if (!secrets?.getOrgPublicKey || !secrets.createOrUpdateOrgSecret || !secrets.listOrgSecrets) { + throw new Error('GitHub organization Secret API is unavailable or the setup PAT lacks organization Secret permissions.'); + } + if (target.organizationVisibility === 'selected' && target.repositoryId === undefined) { + throw new Error('The repository ID is required for selected organization Secret access.'); + } + const existing = new Map((await listCollection(client, secrets.listOrgSecrets, { org: owner, per_page: 30 }, 'secrets')) + .map(secret => [secret.name, secret])); + const publicKey = await secrets.getOrgPublicKey({ org: owner }); + let created = 0; + let updated = 0; + const errors = []; + for (const credential of credentials) { + try { + const current = existing.get(credential.name); + const visibility = current?.visibility ?? target.organizationVisibility; + await secrets.createOrUpdateOrgSecret({ + org: owner, + secret_name: credential.name, + encrypted_value: encryptSecret(credential.value, publicKey.data.key), + key_id: publicKey.data.key_id, + visibility, + ...(visibility === 'selected' && target.repositoryId !== undefined && !current + ? { selected_repository_ids: [target.repositoryId] } + : {}), + }); + if (visibility === 'selected' && target.repositoryId !== undefined && secrets.addSelectedRepoToOrgSecret) { + await secrets.addSelectedRepoToOrgSecret({ org: owner, secret_name: credential.name, repository_id: target.repositoryId }); + } + if (current) + updated += 1; + else + created += 1; + } + catch (error) { + errors.push(`Error configuring organization Secret ${credential.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, skipped: 0, errors }; + } /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ async upsert(owner, repository, token, variables) { return this.upsertVariables(owner, repository, token, variables); } async upsertVariables(owner, repository, token, variables) { const client = this.githubClient.getClient(token); - const existing = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); - const existingValues = new Map(existing.data.variables.map(variable => [variable.name, variable.value])); + const existingVariables = await listCollection(client, client.rest.actions.listRepoVariables, { owner, repo: repository, per_page: 100 }, 'variables'); + const existingValues = new Map(existingVariables.map(variable => [variable.name, variable.value])); let created = 0; let updated = 0; const errors = []; @@ -69175,8 +69478,98 @@ class RepositoryVariablesRepository { } return { created, updated, errors }; } + async upsertScopedVariables(owner, repository, token, target, variables) { + if (target.scope === 'repository') + return this.upsert(owner, repository, token, variables); + const client = this.githubClient.getClient(token); + const actions = client.rest.actions; + if (!actions.listOrgVariables || !actions.createOrUpdateOrgVariable) { + throw new Error('GitHub organization Variable API is unavailable or the setup PAT lacks organization Variable permissions.'); + } + if (target.organizationVisibility === 'selected' && target.repositoryId === undefined) { + throw new Error('The repository ID is required for selected organization Variable access.'); + } + const existing = new Map((await listCollection(client, actions.listOrgVariables, { org: owner, per_page: 30 }, 'variables')) + .map(variable => [variable.name, variable])); + let created = 0; + let updated = 0; + const errors = []; + for (const variable of variables) { + try { + const current = existing.get(variable.name); + const visibility = current?.visibility ?? target.organizationVisibility; + await actions.createOrUpdateOrgVariable({ + org: owner, + name: variable.name, + value: variable.value, + visibility, + ...(visibility === 'selected' && target.repositoryId !== undefined && !current + ? { selected_repository_ids: [target.repositoryId] } + : {}), + }); + if (visibility === 'selected' && target.repositoryId !== undefined && actions.addSelectedRepoToOrgVariable) { + await actions.addSelectedRepoToOrgVariable({ org: owner, name: variable.name, repository_id: target.repositoryId }); + } + if (current) + updated += 1; + else + created += 1; + } + catch (error) { + errors.push(`Error configuring organization Variable ${variable.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, errors }; + } + async listOrganizationSecrets(client, owner, repository, ownerType) { + if (ownerType !== 'Organization') + return { resources: [], access: 'not_applicable' }; + const list = client.rest.secrets?.listRepoOrganizationSecrets; + if (!list) + return { resources: [], access: 'unknown' }; + try { + return { resources: await listCollection(client, list, { owner, repo: repository, per_page: 30 }, 'secrets'), access: 'available' }; + } + catch { + return { resources: [], access: 'unavailable' }; + } + } + async listOrganizationVariables(client, owner, repository, ownerType) { + if (ownerType !== 'Organization') + return { resources: [], access: 'not_applicable' }; + const list = client.rest.actions.listRepoOrganizationVariables; + if (!list) + return { resources: [], access: 'unknown' }; + try { + return { resources: await listCollection(client, list, { owner, repo: repository, per_page: 30 }, 'variables'), access: 'available' }; + } + catch { + return { resources: [], access: 'unavailable' }; + } + } } exports.RepositoryVariablesRepository = RepositoryVariablesRepository; +async function listCollection(client, method, parameters, key) { + if (client.paginate) + return client.paginate(method, parameters); + const response = await method(parameters); + return Array.isArray(response.data) ? response.data : response.data[key] ?? []; +} +function normalizeOwnerType(value) { + return value === 'Organization' ? 'Organization' : value === 'User' ? 'User' : 'Unknown'; +} +function normalizeRepositoryVisibility(value) { + return value === 'public' || value === 'private' || value === 'internal' ? value : 'unknown'; +} +function combineOrganizationAccess(secrets, variables) { + if (secrets === 'not_applicable' && variables === 'not_applicable') + return 'not_applicable'; + if (secrets === 'available' || variables === 'available') + return 'available'; + if (secrets === 'unavailable' || variables === 'unavailable') + return 'unavailable'; + return 'unknown'; +} /** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ function encryptSecret(value, base64PublicKey) { const publicKey = Buffer.from(base64PublicKey, 'base64'); @@ -70403,7 +70796,7 @@ const github_identity_client_factory_2 = __nccwpck_require__(93081); function createInitialSetupCompositionRoot() { const labelProvisioning = new issue_label_provisioning_repository_1.IssueLabelProvisioningRepository((0, github_issue_client_factory_1.createIssueLabelProvisioningClient)()); const repositoryConfiguration = new repository_variables_repository_1.RepositoryVariablesRepository((0, github_identity_client_factory_2.createRepositoryVariablesClient)()); - return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter(), repositoryConfiguration, repositoryConfiguration); + return (0, initial_setup_use_case_composition_1.composeInitialSetupUseCase)(new authenticated_user_repository_1.AuthenticatedUserRepository((0, github_identity_client_factory_1.createAuthenticatedUserClient)()), labelProvisioning, new issue_type_repository_1.IssueTypeRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new git_cli_repository_1.GitCliRepository(), new repository_default_branch_repository_1.RepositoryDefaultBranchRepository((0, github_release_client_factory_1.createReleaseClient)()), new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()), new setup_workspace_adapter_1.SetupWorkspaceAdapter(), repositoryConfiguration, repositoryConfiguration, repositoryConfiguration); } diff --git a/build/github_action/src/application/policies/setup_configuration_policy.d.ts b/build/github_action/src/application/policies/setup_configuration_policy.d.ts index ade1bcf4..67ef0424 100644 --- a/build/github_action/src/application/policies/setup_configuration_policy.d.ts +++ b/build/github_action/src/application/policies/setup_configuration_policy.d.ts @@ -1,7 +1,8 @@ import type { AgentTask } from '../../domain/agent'; -import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement } from '../../domain/setup'; +import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement, SetupResourceScope, SetupResourceStoragePolicy, SetupStorageConfiguration, SetupRemoteConfiguration, SetupResourceTarget } from '../../domain/setup'; export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; +export declare function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration; export declare function createDefaultSetupConfiguration(): SetupConfiguration; export type SetupConfigurationOverrides = { features?: Partial; @@ -13,6 +14,10 @@ export type SetupConfigurationOverrides = { manageRepositoryVariables?: boolean; manageRepositorySecrets?: boolean; actionInputs?: Record; + storage?: { + secrets?: Partial; + variables?: Partial; + }; }; export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; @@ -21,3 +26,16 @@ export declare function buildSetupPlan(configuration: SetupConfiguration): Setup export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; +export declare function resolveSetupResourceScope(policy: SetupResourceStoragePolicy, name: string): SetupResourceScope; +export type SetupResourceKind = 'secret' | 'variable'; +export declare function getSetupResourceStoragePolicy(configuration: SetupConfiguration, kind: SetupResourceKind): SetupResourceStoragePolicy; +export declare function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration; +export declare function resolveSetupResourceTarget(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): SetupResourceTarget; +export declare function setupResourceExists(remote: SetupRemoteConfiguration | undefined, kind: SetupResourceKind, name: string): { + repository: boolean; + organization: boolean; + effective?: SetupResourceScope; +}; +export declare function shouldUpsertSetupResource(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): boolean; +export declare function validateSetupStorageAgainstRemote(configuration: SetupConfiguration, remote: SetupRemoteConfiguration): string[]; +export declare function usesOrganizationStorage(configuration: SetupConfiguration): boolean; diff --git a/build/github_action/src/application/ports/setup_wizard_ports.d.ts b/build/github_action/src/application/ports/setup_wizard_ports.d.ts index 3ede0673..081ad35b 100644 --- a/build/github_action/src/application/ports/setup_wizard_ports.d.ts +++ b/build/github_action/src/application/ports/setup_wizard_ports.d.ts @@ -1,10 +1,19 @@ -import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, DoctorCheck } from '../../domain/setup'; +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, DoctorCheck, SetupCredentialRequirement, SetupResourceTarget, SetupRemoteConfiguration, SetupStorageConfiguration, SetupVariable } from '../../domain/setup'; export interface SetupPromptPort { collect(defaults: SetupConfiguration): Promise; showPlan(plan: SetupPlan): void; confirm(plan: SetupPlan): Promise; close(): void; } +export interface SetupStoragePromptPort { + chooseStorage(defaults: SetupStorageConfiguration, remote: SetupRemoteConfiguration, variables: readonly SetupVariable[], requirements: readonly SetupCredentialRequirement[], managed?: { + secrets: boolean; + variables: boolean; + }): Promise; +} +export interface SetupRemoteConfigurationReadPort { + inspect(owner: string, repository: string, token: string): Promise; +} export interface SetupCredentialPromptPort { requestSetupPat(): Promise; explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; @@ -21,6 +30,12 @@ export interface SetupRepositorySecretsPort { skipped: number; errors: string[]; }>; + upsertScopedSecrets?(owner: string, repository: string, token: string, target: SetupResourceTarget, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; } export interface SetupRepositoryConfigurationReadPort { listVariables(owner: string, repository: string, token: string): Promise; + upsertScopedVariables?(owner: string, repository: string, token: string, target: SetupResourceTarget, variables: readonly { + name: string; + value: string; + }[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; } diff --git a/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts b/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts index aab353e2..ba6bb3d7 100644 --- a/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts +++ b/build/github_action/src/application/usecases/actions/initial_setup_use_case.d.ts @@ -6,7 +6,7 @@ import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { ParamUseCase } from '../base/param_usecase'; -import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; /** Application boundary for provisioning a repository for Copilot automation. */ export declare class InitialSetupUseCase implements ParamUseCase { private readonly authenticatedUserPort; @@ -18,7 +18,8 @@ export declare class InitialSetupUseCase implements ParamUseCase; } diff --git a/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts b/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts index 5bc88b49..94717181 100644 --- a/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts +++ b/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts @@ -5,7 +5,7 @@ import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports/repository_release_ports'; import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; -import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; export interface InitialSetupWorkflowDependencies { authenticatedUserPort: AuthenticatedUserPort; initialLabelProvisioningPort: InitialLabelProvisioningPort; @@ -16,6 +16,7 @@ export interface InitialSetupWorkflowDependencies { setupWorkspacePort: SetupWorkspacePort; setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; setupRepositorySecretsPort?: SetupRepositorySecretsPort; + setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; } /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ export declare function runInitialSetupWorkflow(param: Execution, dependencies: InitialSetupWorkflowDependencies): Promise; diff --git a/build/github_action/src/application/usecases/setup/doctor_use_case.d.ts b/build/github_action/src/application/usecases/setup/doctor_use_case.d.ts index 16692f6f..83eb717f 100644 --- a/build/github_action/src/application/usecases/setup/doctor_use_case.d.ts +++ b/build/github_action/src/application/usecases/setup/doctor_use_case.d.ts @@ -1,5 +1,5 @@ import type { SetupConfiguration } from '../../../domain/setup'; -import type { DoctorOutputPort, SetupCredentialValidationPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +import type { DoctorOutputPort, SetupCredentialValidationPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRemoteConfigurationReadPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; export interface DoctorRequest { owner: string; @@ -14,6 +14,7 @@ export declare class SetupDoctorUseCase { private readonly workspace; private readonly output; private readonly remoteHealth?; - constructor(validation: SetupCredentialValidationPort, secrets: SetupRepositorySecretsPort, variables: SetupRepositoryConfigurationReadPort, workspace: SetupWorkspacePort, output: DoctorOutputPort, remoteHealth?: SetupRemoteCredentialHealthPort | undefined); + private readonly remoteConfigurationReader?; + constructor(validation: SetupCredentialValidationPort, secrets: SetupRepositorySecretsPort, variables: SetupRepositoryConfigurationReadPort, workspace: SetupWorkspacePort, output: DoctorOutputPort, remoteHealth?: SetupRemoteCredentialHealthPort | undefined, remoteConfigurationReader?: SetupRemoteConfigurationReadPort | undefined); execute(request: DoctorRequest): Promise; } diff --git a/build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts b/build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts index a6a3295f..d8e4c4f7 100644 --- a/build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts +++ b/build/github_action/src/application/usecases/setup/setup_credentials_use_case.d.ts @@ -1,5 +1,6 @@ import type { SetupCredentialCheck, SetupCredentialCollection, SetupCredentialRequirement } from '../../../domain/setup'; import type { SetupCredentialPromptPort, SetupCredentialValidationPort, SetupRepositorySecretsPort, SetupRemoteCredentialHealthPort } from '../../ports/setup_wizard_ports'; +import type { SetupRemoteConfiguration } from '../../../domain/setup'; export interface SetupCredentialsRequest { owner: string; repository: string; @@ -7,6 +8,7 @@ export interface SetupCredentialsRequest { requirements: readonly SetupCredentialRequirement[]; manageSecrets: boolean; ref?: string; + remoteConfiguration?: SetupRemoteConfiguration; } export interface SetupCredentialsResult { collection: SetupCredentialCollection; diff --git a/build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts b/build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts index ad5dfab9..a8cc5c19 100644 --- a/build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts +++ b/build/github_action/src/application/usecases/setup/setup_wizard_use_case.d.ts @@ -1,14 +1,24 @@ -import type { SetupPromptPort } from '../../ports/setup_wizard_ports'; -import type { SetupConfiguration, SetupPlan } from '../../../domain/setup'; +import type { SetupPromptPort, SetupRemoteConfigurationReadPort, SetupStoragePromptPort } from '../../ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan, SetupRemoteConfiguration } from '../../../domain/setup'; import { type SetupConfigurationOverrides } from '../../policies/setup_configuration_policy'; export interface SetupWizardRequest { overrides?: SetupConfigurationOverrides; skipRepositoryVariables?: boolean; + skipRepositorySecrets?: boolean; + remoteTarget?: { + owner: string; + repository: string; + token: string; + }; } export declare class SetupWizardUseCase { private readonly prompt; - constructor(prompt: SetupPromptPort); + private readonly remoteConfigurationReader?; + private readonly storagePrompt?; + private lastRemoteConfiguration; + constructor(prompt: SetupPromptPort, remoteConfigurationReader?: SetupRemoteConfigurationReadPort | undefined, storagePrompt?: SetupStoragePromptPort | undefined); collect(request?: SetupWizardRequest): Promise; plan(configuration: SetupConfiguration): SetupPlan; + remoteConfiguration(): SetupRemoteConfiguration | undefined; close(): void; } diff --git a/build/github_action/src/cli/commands/setup_policy.d.ts b/build/github_action/src/cli/commands/setup_policy.d.ts index 670911a0..42c06cb1 100644 --- a/build/github_action/src/cli/commands/setup_policy.d.ts +++ b/build/github_action/src/cli/commands/setup_policy.d.ts @@ -1,6 +1,6 @@ import type { GitInfo } from '../../cli_context'; -import type { SetupConfiguration, SetupCredentialCollection } from '../../domain/setup'; +import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration } from '../../domain/setup'; export interface SetupCommandOptions { debug?: boolean; } -export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string, configuration?: SetupConfiguration, credentials?: SetupCredentialCollection, approvedWorkflowFiles?: readonly string[]): Record | undefined; +export declare function buildSetupParams(options: SetupCommandOptions, gitInfo: GitInfo, token: string, configuration?: SetupConfiguration, credentials?: SetupCredentialCollection, approvedWorkflowFiles?: readonly string[], remoteConfiguration?: SetupRemoteConfiguration): Record | undefined; diff --git a/build/github_action/src/cli/setup_prompt_adapter.d.ts b/build/github_action/src/cli/setup_prompt_adapter.d.ts index 1ad2dd3d..3665e0e4 100644 --- a/build/github_action/src/cli/setup_prompt_adapter.d.ts +++ b/build/github_action/src/cli/setup_prompt_adapter.d.ts @@ -1,17 +1,21 @@ -import type { SetupCredentialPromptPort, SetupPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort } from '../application/ports/setup_wizard_ports'; -import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison } from '../domain/setup'; +import type { SetupCredentialPromptPort, SetupStoragePromptPort, SetupPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort } from '../application/ports/setup_wizard_ports'; +import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, SetupStorageConfiguration, SetupRemoteConfiguration, SetupVariable } from '../domain/setup'; export interface SetupPromptAdapterOptions { interactive?: boolean; assumeYes?: boolean; credentialValues?: Record; } -export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { +export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupStoragePromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { private readonly interactive; private readonly assumeYes; private readonly readline; private readonly credentialValues; constructor(options?: SetupPromptAdapterOptions); collect(defaults: SetupConfiguration): Promise; + chooseStorage(defaults: SetupStorageConfiguration, remote: SetupRemoteConfiguration, variables: readonly SetupVariable[], requirements: readonly SetupCredentialRequirement[], managed?: { + secrets: boolean; + variables: boolean; + }): Promise; showPlan(plan: SetupPlan): void; confirm(plan: SetupPlan): Promise; requestSetupPat(): Promise; @@ -29,4 +33,5 @@ export declare class SetupPromptAdapter implements SetupPromptPort, SetupCredent private askNumber; private askBoolean; private askChoice; + private chooseStoragePolicy; } diff --git a/build/github_action/src/data/repository/repository_variables_repository.d.ts b/build/github_action/src/data/repository/repository_variables_repository.d.ts index bd7ceeb5..6dd314fe 100644 --- a/build/github_action/src/data/repository/repository_variables_repository.d.ts +++ b/build/github_action/src/data/repository/repository_variables_repository.d.ts @@ -1,8 +1,8 @@ -import type { SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; -import type { SetupCredentialValue } from '../../domain/setup'; +import type { SetupRemoteConfigurationReadPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialValue, SetupRemoteConfiguration, SetupResourceTarget, SetupVariable } from '../../domain/setup'; import type { GithubClientPort } from '../../infrastructure/github/ports/github_client_provider_port'; import type { GithubRepositoryVariablesClient } from '../../infrastructure/github/ports/github_repository_variables_protocol'; -export declare class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort { +export declare class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort, SetupRemoteConfigurationReadPort { private readonly githubClient; constructor(githubClient: GithubClientPort); list(owner: string, repository: string, token: string): Promise; @@ -10,12 +10,19 @@ export declare class RepositoryVariablesRepository implements SetupRepositoryVar name: string; value?: string; }[]>; + inspect(owner: string, repository: string, token: string): Promise; upsertSecrets(owner: string, repository: string, token: string, credentials: readonly SetupCredentialValue[]): Promise<{ created: number; updated: number; skipped: number; errors: string[]; }>; + upsertScopedSecrets(owner: string, repository: string, token: string, target: SetupResourceTarget, credentials: readonly SetupCredentialValue[]): Promise<{ + created: number; + updated: number; + skipped: number; + errors: string[]; + }>; /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ upsert(owner: string, repository: string, token: string, variables: readonly { name: string; @@ -26,6 +33,13 @@ export declare class RepositoryVariablesRepository implements SetupRepositoryVar errors: string[]; }>; private upsertVariables; + upsertScopedVariables(owner: string, repository: string, token: string, target: SetupResourceTarget, variables: readonly SetupVariable[]): Promise<{ + created: number; + updated: number; + errors: string[]; + }>; + private listOrganizationSecrets; + private listOrganizationVariables; } /** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ export declare function encryptSecret(value: string, base64PublicKey: string): string; diff --git a/build/github_action/src/domain/setup.d.ts b/build/github_action/src/domain/setup.d.ts index 668d18ad..12b58f46 100644 --- a/build/github_action/src/domain/setup.d.ts +++ b/build/github_action/src/domain/setup.d.ts @@ -60,6 +60,27 @@ export interface SetupConfiguration { manageRepositorySecrets: boolean; /** Extra non-secret action inputs accepted by config files for advanced use cases. */ actionInputs: Record; + /** Independent storage policies for non-sensitive variables and secrets. */ + storage: SetupStorageConfiguration; +} +export type SetupResourceScope = 'repository' | 'organization'; +export type SetupOrganizationVisibility = 'all' | 'private' | 'selected'; +export interface SetupResourceStoragePolicy { + defaultScope: SetupResourceScope; + organizationVisibility: SetupOrganizationVisibility; + /** Keep an already-effective resource instead of creating a shadowing override. */ + preserveExisting: boolean; + /** Per-resource exceptions for mixed repository/organization configurations. */ + overrides: Record; +} +export interface SetupResourceTarget { + scope: SetupResourceScope; + organizationVisibility: SetupOrganizationVisibility; + repositoryId?: number; +} +export interface SetupStorageConfiguration { + secrets: SetupResourceStoragePolicy; + variables: SetupResourceStoragePolicy; } export type SetupCredentialKind = 'workflowPat' | 'apiKey'; export type SetupCredentialStatus = 'valid' | 'invalid' | 'missing' | 'unverifiable' | 'not_required'; @@ -76,6 +97,7 @@ export interface SetupCredentialCheck { status: SetupCredentialStatus; message: string; account?: string; + sourceScope?: SetupResourceScope; } export interface SetupCredentialValue { name: string; @@ -86,6 +108,20 @@ export interface SetupCredentialCollection { workflowPat?: SetupCredentialValue; apiKeys: SetupCredentialValue[]; } +export type SetupOwnerType = 'User' | 'Organization' | 'Unknown'; +export type SetupRepositoryVisibility = 'public' | 'private' | 'internal' | 'unknown'; +export interface SetupRemoteConfiguration { + ownerType: SetupOwnerType; + repositoryId?: number; + repositoryVisibility: SetupRepositoryVisibility; + repositorySecrets: readonly string[]; + organizationSecrets: readonly string[]; + repositoryVariables: readonly SetupVariable[]; + organizationVariables: readonly SetupVariable[]; + organizationAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; + organizationSecretsAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; + organizationVariablesAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; +} export interface SetupWorkflowComparison { file: string; destination: string; diff --git a/build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts b/build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts index 0c0bf531..634eec14 100644 --- a/build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts +++ b/build/github_action/src/infrastructure/composition/setup_credentials_composition_root.d.ts @@ -1,3 +1,4 @@ import { SetupCredentialsUseCase } from '../../application/usecases/setup/setup_credentials_use_case'; -import type { SetupCredentialPromptPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialPromptPort, SetupRemoteConfigurationReadPort } from '../../application/ports/setup_wizard_ports'; export declare function createSetupCredentialsUseCase(prompt: SetupCredentialPromptPort): SetupCredentialsUseCase; +export declare function createSetupRemoteConfigurationReadPort(): SetupRemoteConfigurationReadPort; diff --git a/build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts b/build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts index 37799fed..ed681400 100644 --- a/build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts +++ b/build/github_action/src/infrastructure/github/ports/github_repository_variables_protocol.d.ts @@ -2,8 +2,30 @@ export interface GithubRepositoryVariable { name: string; value?: string; } +export interface GithubOrganizationResource { + name: string; + value?: string; + visibility?: 'all' | 'private' | 'selected'; + selected_repositories_url?: string; +} +export interface GithubRepositoryMetadata { + id?: number; + visibility?: string; + owner?: { + type?: string; + }; +} +export interface GithubActionsPublicKey { + key_id: string; + key: string; +} export interface GithubRepositoryVariablesClient { rest: { + repos?: { + get(parameters: Record): Promise<{ + data: GithubRepositoryMetadata; + }>; + }; actions: { listRepoVariables(parameters: Record): Promise<{ data: { @@ -12,6 +34,18 @@ export interface GithubRepositoryVariablesClient { }>; createRepoVariable(parameters: Record): Promise; updateRepoVariable(parameters: Record): Promise; + listRepoOrganizationVariables?: (parameters: Record) => Promise<{ + data: { + variables: GithubOrganizationResource[]; + }; + }>; + listOrgVariables?: (parameters: Record) => Promise<{ + data: { + variables: GithubOrganizationResource[]; + }; + }>; + createOrUpdateOrgVariable?: (parameters: Record) => Promise; + addSelectedRepoToOrgVariable?: (parameters: Record) => Promise; }; secrets?: { listRepoSecrets(parameters: Record): Promise<{ @@ -20,14 +54,32 @@ export interface GithubRepositoryVariablesClient { }; }>; getRepoPublicKey(parameters: Record): Promise<{ + data: GithubActionsPublicKey; + }>; + createOrUpdateRepoSecret(parameters: Record): Promise; + listRepoOrganizationSecrets?: (parameters: Record) => Promise<{ data: { - key_id: string; - key: string; + secrets: GithubOrganizationResource[]; }; }>; - createOrUpdateRepoSecret(parameters: Record): Promise; + listOrgSecrets?: (parameters: Record) => Promise<{ + data: { + secrets: GithubOrganizationResource[]; + }; + }>; + getOrgPublicKey?: (parameters: Record) => Promise<{ + data: GithubActionsPublicKey; + }>; + createOrUpdateOrgSecret?: (parameters: Record) => Promise; + addSelectedRepoToOrgSecret?: (parameters: Record) => Promise; }; }; + paginate?: (method: (parameters: Record) => Promise<{ + data: T[] | { + variables?: T[]; + secrets?: T[]; + }; + }>, parameters: Record) => Promise; } export interface GithubRepositorySecret { name: string; diff --git a/docs/authentication.mdx b/docs/authentication.mdx index cd4a794e..79c417ec 100644 --- a/docs/authentication.mdx +++ b/docs/authentication.mdx @@ -24,7 +24,7 @@ For comment-driven assistance, read-only commands are available to anyone who ca - The person running setup needs a separate fine-grained PAT. Give it only the permissions required by the selected setup features: repository Metadata read and repository Contents/Workflows read for inspection; Issues write for labels; Variables write for Repository Variables; Secrets read/write when provisioning Secrets; Actions read/write when checking or dispatching credential health; and organization Issue Types or Projects permissions only when those integrations are selected. Contents write and Workflows write are required only if the operator chooses to modify workflow files through the GitHub API. + The person running setup needs a separate fine-grained PAT. Give it only the permissions required by the selected setup features: repository Metadata read and repository Contents/Workflows read for inspection; Issues write for labels; Variables write for Repository Variables; Secrets read/write when provisioning Secrets; Actions read/write when checking or dispatching credential health; and organization Issue Types or Projects permissions only when those integrations are selected. If setup will use organization-level Actions Secrets or Variables, the token also needs the corresponding organization Actions Secrets/Variables read and write permissions. Organization scope is valid only for repositories owned by an organization; setup detects personal repositories and stops before attempting organization writes. Contents write and Workflows write are required only if the operator chooses to modify workflow files through the GitHub API. Enter it in the hidden prompt, or use `--token`/`PERSONAL_ACCESS_TOKEN` for automation. It remains in memory for the command and is not written to `.env`, a config file, or the `PAT` Secret. diff --git a/docs/configuration-checklist.mdx b/docs/configuration-checklist.mdx index cc562af8..52331d0e 100644 --- a/docs/configuration-checklist.mdx +++ b/docs/configuration-checklist.mdx @@ -12,11 +12,15 @@ description: Required checks before enabling Copilot automation. - [ ] `AGENT_ALLOWED_MODEL_PROVIDERS` contains the selected model provider. - [ ] `AGENT_ALLOWED_MODELS` contains the exact qualified model. - [ ] `agent-command` is empty unless an audited command override is required. +- [ ] Secrets and Variables have been chosen independently as repository- or organization-scoped resources. +- [ ] Organization-scoped resources use an intentional visibility (`selected`, `private`, or `all`) and the setup PAT has organization Actions permissions. +- [ ] Existing organization resources are inherited deliberately; named repository overrides are listed when needed. ## Credentials - [ ] Credentials are configured as secrets or as a local self-hosted credential store. - [ ] No session file or token is copied into GitHub Secrets. +- [ ] Secret values are never placed in `.copilot-setup.yml`, command configuration, or Variables. - [ ] Fork workflows cannot access write credentials. - [ ] `pull_request_target` does not execute attacker-controlled checkout content. diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 79dfe66c..07bd96bb 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -131,6 +131,29 @@ Copilot provides extensive configuration options to customize your workflow. Use +## GitHub Actions resource scope + +`copilot setup` treats Secrets and Variables as two independent resource classes. Each class can default to the repository or the organization, and individual names can override that default. Organization resources use GitHub's `selected`, `private`, or `all` visibility; `selected` is the recommended least-privilege choice. + +```yaml +manageRepositorySecrets: true +manageRepositoryVariables: true +storage: + secrets: + defaultScope: organization + organizationVisibility: selected + preserveExisting: true + variables: + defaultScope: repository + preserveExisting: true + overrides: + OPENAI_API_KEY: organization +``` + +The wizard first inspects the repository and reports repository-scoped resources, organization resources available to that repository, repository visibility, and access errors. It then asks separately about Secret and Variable storage. Repository resources take precedence over organization resources. With `preserveExisting: true`, an effective organization resource is inherited instead of being shadowed by a new repository value; add a name under `storage.secrets.overrides` or `storage.variables.overrides` when a repository-specific value is intentional. + +Organization storage is available only for organization-owned repositories and requires organization Actions permissions on the setup PAT. If only one class should be global, set that class to `organization` and leave the other at `repository`. `--skip-secrets` and `--skip-variables` disable their respective setup operations without changing the other class. + ## Complete input reference The tables above group the most commonly changed inputs. The following inputs are diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index 40382760..7d328ccb 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -118,7 +118,7 @@ All commands support `-h, --help`. The `-d, --debug` option enables additional d ### `copilot setup` -Initializes the current GitHub repository through an interactive English-language wizard. It can select the workflows and templates to install, route each Copilot task to Codex, OpenCode, or Cursor, configure repository behavior and AI policy, upsert non-sensitive GitHub Repository Variables, verify access, and create the configured labels and issue types. If the repository has no version tags, setup asks whether it should create `v1.0.0`. +Initializes the current GitHub repository through an interactive English-language wizard. It can select the workflows and templates to install, route each Copilot task to Codex, OpenCode, or Cursor, configure repository behavior and AI policy, inspect and provision GitHub Actions Variables and Secrets at repository or organization scope, verify access, and create the configured labels and issue types. If the repository has no version tags, setup asks whether it should create `v1.0.0`. | Option | Required | Description | | --- | --- | --- | @@ -132,6 +132,12 @@ Initializes the current GitHub repository through an interactive English-languag | `--dry-run` | No | Print the complete plan without changing files or GitHub. A token is not required. | | `--skip-variables` | No | Copy files and provision metadata without changing Repository Variables. | | `--skip-secrets` | No | Do not validate or create/update repository Secrets. | +| `--variables-scope ` | No | Default scope for Variables: `repository` or `organization`. | +| `--secrets-scope ` | No | Default scope for Secrets: `repository` or `organization`. | +| `--variables-visibility ` | No | Organization Variable visibility: `selected`, `private`, or `all`. | +| `--secrets-visibility ` | No | Organization Secret visibility: `selected`, `private`, or `all`. | +| `--variable-scope ` | No | Repeat to mix repository and organization scopes per Variable. | +| `--secret-scope ` | No | Repeat to mix repository and organization scopes per Secret. | | `--update-workflows` | No | Approve updates to changed setup workflows already present in the repository. | | `--workflow-pat ` | No | Workflow PAT for non-interactive setup; prefer the hidden prompt. | | `--secret ` | No | Repeat for provider credentials in non-interactive setup; values can appear in shell history. | @@ -140,7 +146,7 @@ Initializes the current GitHub repository through an interactive English-languag copilot setup ``` -Run it from the target repository root. Existing setup files are not overwritten unless you approve the update prompt or pass `--update-workflows`. The interactive flow installs the selected workflows/templates, asks for agent routing and operational settings, validates the separate workflow PAT and selected provider credentials, and creates or updates the required remote Secrets and Variables. +Run it from the target repository root. Existing setup files are not overwritten unless you approve the update prompt or pass `--update-workflows`. The interactive flow installs the selected workflows/templates, asks for agent routing and operational settings, inspects the repository's effective Actions resources, and asks separately where Secrets and Variables should live. Organization scope requires an organization-owned repository and the corresponding organization permissions on the setup PAT. Existing repository resources take precedence over organization resources; setup preserves an inherited organization resource unless you explicitly request a repository override. The wizard covers these features: `issues`, `pullRequests`, `commits`, `issueComments`, `pullRequestComments`, `release`, `hotfix`, `agentProvisioning`, `credentialHealth`, `issueTemplates`, and `pullRequestTemplate`. The agent tasks are `planner`, `findings`, `reviewer`, `fixer`, `tester`, and `release`; each can use any of the three supported runtimes independently. Model provider, model, effort, branch strategy, locales, AI ignore patterns, project columns, Bugbot policy, provisioning mode, and initial-tag creation are also configurable. Cursor is available as an experimental runtime and is called out in the review plan with its extra credential/checksum requirements. @@ -158,7 +164,11 @@ For unattended credential provisioning, keep the setup PAT in a protected CI sec copilot setup --non-interactive --yes --update-workflows \ --token "$SETUP_PAT" \ --workflow-pat "$WORKFLOW_PAT" \ - --secret "OPENAI_API_KEY=$OPENAI_API_KEY" + --secret "OPENAI_API_KEY=$OPENAI_API_KEY" \ + --secrets-scope organization \ + --secrets-visibility selected \ + --variables-scope repository \ + --variable-scope AGENT_PROVIDER=repository ``` The explicit `--workflow-pat` and `--secret` options are provided for automation and can appear in process listings or shell history. The interactive hidden prompt is safer for a human operator. @@ -205,9 +215,22 @@ ai: pullRequestDescriptionMode: append createInitialTag: true manageRepositoryVariables: true + +# Secrets and Variables are configured independently. Organization resources +# require an organization-owned repository and organization Actions permissions. +storage: + secrets: + defaultScope: organization + organizationVisibility: selected + preserveExisting: true + variables: + defaultScope: repository + preserveExisting: true + overrides: + OPENAI_API_KEY: organization ``` -Run it with `copilot setup --config .copilot-setup.yml`. The wizard rejects values that look like tokens, API keys, passwords, or other credential material. Secret values are accepted only through the hidden prompt or explicit CLI inputs and are written directly to GitHub Secrets after validation; they are never written to repository files or Variables. The required secret names are shown in the final plan; normally they include `PAT` plus the credentials needed by the selected runtime/model providers. +Run it with `copilot setup --config .copilot-setup.yml`. The wizard rejects values that look like tokens, API keys, passwords, or other credential material. Secret values are accepted only through the hidden prompt or explicit CLI inputs and are written directly to GitHub Secrets after validation; they are never written to repository files or Variables. The required secret names are shown in the final plan; normally they include `PAT` plus the credentials needed by the selected runtime/model providers. When a repository already inherits an organization resource, `preserveExisting: true` avoids creating a repository shadow; use `storage.*.overrides` when one named resource needs a different scope. ### `copilot reconcile` diff --git a/src/application/policies/__tests__/setup_configuration_policy.test.ts b/src/application/policies/__tests__/setup_configuration_policy.test.ts index aa060d38..cbd73b54 100644 --- a/src/application/policies/__tests__/setup_configuration_policy.test.ts +++ b/src/application/policies/__tests__/setup_configuration_policy.test.ts @@ -5,6 +5,9 @@ import { buildSetupRepositoryVariables, createDefaultSetupConfiguration, mergeSetupConfiguration, + resolveSetupResourceTarget, + shouldUpsertSetupResource, + validateSetupStorageAgainstRemote, validateSetupConfiguration, } from '../setup_configuration_policy'; import type { SetupConfigurationOverrides } from '../setup_configuration_policy'; @@ -117,4 +120,138 @@ describe('setup configuration policy', () => { ])); expect(buildSetupActionInputs(configuration)['ai-pull-request-description-mode']).toBe('append'); }); + + it('keeps independent repository/organization storage policies and mixed overrides', () => { + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + storage: { + secrets: { defaultScope: 'organization', organizationVisibility: 'private' }, + variables: { overrides: { AGENT_PROVIDER: 'repository' } }, + }, + }); + const remote = { + ownerType: 'Organization' as const, + repositoryId: 42, + repositoryVisibility: 'private' as const, + repositorySecrets: [], + organizationSecrets: ['PAT'], + repositoryVariables: [], + organizationVariables: [{ name: 'AGENT_PROVIDER', value: 'codex' }], + organizationAccess: 'available' as const, + organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + + expect(resolveSetupResourceTarget(configuration, 'secret', 'PAT', remote)).toEqual({ + scope: 'organization', organizationVisibility: 'private', repositoryId: 42, + }); + expect(resolveSetupResourceTarget(configuration, 'variable', 'AGENT_PROVIDER', remote).scope).toBe('repository'); + expect(shouldUpsertSetupResource(configuration, 'secret', 'PAT', remote)).toBe(true); + expect(shouldUpsertSetupResource(configuration, 'variable', 'AGENT_PROVIDER', remote)).toBe(true); + expect(validateSetupStorageAgainstRemote(configuration, remote)).toEqual([]); + }); + + it('preserves an inherited organization resource unless an override is explicit', () => { + const configuration = createDefaultSetupConfiguration(); + const remote = { + ownerType: 'Organization' as const, + repositoryId: 42, + repositoryVisibility: 'private' as const, + repositorySecrets: [], + organizationSecrets: [], + repositoryVariables: [], + organizationVariables: [{ name: 'AGENT_PROVIDER', value: 'codex' }], + organizationAccess: 'available' as const, + organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + + expect(shouldUpsertSetupResource(configuration, 'variable', 'AGENT_PROVIDER', remote)).toBe(false); + const override = mergeSetupConfiguration(configuration, { storage: { variables: { overrides: { AGENT_PROVIDER: 'repository' } } } }); + expect(shouldUpsertSetupResource(override, 'variable', 'AGENT_PROVIDER', remote)).toBe(true); + }); + + it('keeps replacement credentials on the effective repository scope unless scope is explicitly overridden', () => { + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + storage: { secrets: { defaultScope: 'organization' } }, + }); + const remote = { + ownerType: 'Organization' as const, + repositoryId: 42, + repositoryVisibility: 'private' as const, + repositorySecrets: ['PAT'], organizationSecrets: [], repositoryVariables: [], organizationVariables: [], + organizationAccess: 'available' as const, organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + + expect(resolveSetupResourceTarget(configuration, 'secret', 'PAT', remote).scope).toBe('repository'); + const explicit = mergeSetupConfiguration(configuration, { storage: { secrets: { overrides: { PAT: 'organization' } } } }); + expect(resolveSetupResourceTarget(explicit, 'secret', 'PAT', remote).scope).toBe('organization'); + }); + + it('rejects organization storage for personal repositories or unavailable organization permissions', () => { + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + storage: { variables: { defaultScope: 'organization' } }, + }); + const personalRemote = { + ownerType: 'User' as const, + repositoryId: 42, + repositoryVisibility: 'private' as const, + repositorySecrets: [], organizationSecrets: [], repositoryVariables: [], organizationVariables: [], + organizationAccess: 'not_applicable' as const, + organizationSecretsAccess: 'not_applicable' as const, + organizationVariablesAccess: 'not_applicable' as const, + }; + expect(validateSetupStorageAgainstRemote(configuration, personalRemote)).toEqual([ + 'Organization-level variable storage is only available for organization-owned repositories.', + ]); + + const unavailableRemote = { ...personalRemote, ownerType: 'Organization' as const, organizationVariablesAccess: 'unavailable' as const }; + expect(validateSetupStorageAgainstRemote(configuration, unavailableRemote)).toEqual([ + 'The setup PAT cannot inspect organization variables for this repository. Organization variable permissions are required.', + ]); + }); + + it('validates storage policy values and selected access requirements', () => { + const invalid = createDefaultSetupConfiguration() as any; + invalid.storage.secrets.defaultScope = 'tenant'; + invalid.storage.variables.organizationVisibility = 'team'; + invalid.storage.variables.preserveExisting = 'yes'; + invalid.storage.variables.overrides = { 'bad-name': 'tenant' }; + + expect(validateSetupConfiguration(invalid)).toEqual(expect.arrayContaining([ + 'secrets default scope must be repository or organization.', + 'variables organization visibility must be all, private, or selected.', + 'variables preserveExisting must be a boolean.', + 'variables override name bad-name must be an uppercase GitHub Actions name.', + 'variables override bad-name must use repository or organization.', + ])); + + const selected = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + storage: { variables: { defaultScope: 'organization', organizationVisibility: 'selected' } }, + }); + const remote = { + ownerType: 'Organization' as const, repositoryVisibility: 'private' as const, + repositorySecrets: [], organizationSecrets: [], repositoryVariables: [], organizationVariables: [], + organizationAccess: 'available' as const, organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + expect(validateSetupStorageAgainstRemote(selected, remote)).toEqual([ + 'The repository ID is required for selected organization variable access.', + ]); + }); + + it('adds warnings for organization storage, projects, and always-provision mode', () => { + const configuration = createDefaultSetupConfiguration(); + configuration.features.release = false; + configuration.features.hotfix = false; + configuration.projects.ids = 'PVT_example'; + configuration.ai.provisioningMode = 'always'; + configuration.storage.variables.defaultScope = 'organization'; + + expect(buildSetupPlan(configuration).warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Project IDs'), + expect.stringContaining('Always-provision'), + expect.stringContaining('Organization-level'), + ])); + }); }); diff --git a/src/application/policies/setup_configuration_policy.ts b/src/application/policies/setup_configuration_policy.ts index f8f2e9d7..ef94cd21 100644 --- a/src/application/policies/setup_configuration_policy.ts +++ b/src/application/policies/setup_configuration_policy.ts @@ -12,6 +12,11 @@ import type { SetupPlan, SetupVariable, SetupCredentialRequirement, + SetupResourceScope, + SetupResourceStoragePolicy, + SetupStorageConfiguration, + SetupRemoteConfiguration, + SetupResourceTarget, } from '../../domain/setup'; import { SUPPORTED_AGENT_PROVIDERS } from './agent_configuration_validation_policy'; import { normalizePullRequestDescriptionMode } from '../../domain/pull_request_description'; @@ -69,6 +74,24 @@ const SECRET_BY_MODEL_PROVIDER: Readonly> = { openrouter: 'OPENROUTER_API_KEY', }; +const RESOURCE_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/; + +function defaultStoragePolicy(): SetupResourceStoragePolicy { + return { + defaultScope: 'repository', + organizationVisibility: 'selected', + preserveExisting: true, + overrides: {}, + }; +} + +export function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration { + return { + secrets: defaultStoragePolicy(), + variables: defaultStoragePolicy(), + }; +} + export function createDefaultSetupConfiguration(): SetupConfiguration { const defaultRole = (): SetupAgentRoleConfiguration => ({ provider: DEFAULT_AGENT_PROVIDER, @@ -125,6 +148,7 @@ export function createDefaultSetupConfiguration(): SetupConfiguration { manageRepositoryVariables: true, manageRepositorySecrets: true, actionInputs: {}, + storage: createDefaultSetupStorageConfiguration(), }; } @@ -138,6 +162,10 @@ export type SetupConfigurationOverrides = { manageRepositoryVariables?: boolean; manageRepositorySecrets?: boolean; actionInputs?: Record; + storage?: { + secrets?: Partial; + variables?: Partial; + }; }; export function mergeSetupConfiguration( @@ -159,6 +187,10 @@ export function mergeSetupConfiguration( manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, + storage: { + secrets: mergeStoragePolicy(base.storage?.secrets, overrides.storage?.secrets), + variables: mergeStoragePolicy(base.storage?.variables, overrides.storage?.variables), + }, }; } @@ -197,6 +229,7 @@ export function validateSetupConfiguration(configuration: SetupConfiguration): s if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } + errors.push(...validateStorageConfiguration(configuration.storage)); for (const task of SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; if (!SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); @@ -379,7 +412,7 @@ function buildRequiredSetupSecrets(configuration: SetupConfiguration): string[] function buildSetupWarnings(configuration: SetupConfiguration): string[] { const warnings: string[] = []; if (configuration.features.release !== false && configuration.features.hotfix !== false) { - warnings.push('Release and hotfix workflows require the repository secret PAT and a writable token.'); + warnings.push('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); } if (configuration.ai.provisioningMode === 'always') { warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); @@ -390,9 +423,161 @@ function buildSetupWarnings(configuration: SetupConfiguration): string[] { if (SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); } + if (usesOrganizationStorage(configuration)) { + warnings.push('Organization-level Secrets and Variables require organization permissions; selected access is the safest default and repository values take precedence.'); + } return warnings; } +export function resolveSetupResourceScope( + policy: SetupResourceStoragePolicy, + name: string, +): SetupResourceScope { + return policy.overrides[name] ?? policy.defaultScope; +} + +export type SetupResourceKind = 'secret' | 'variable'; + +export function getSetupResourceStoragePolicy( + configuration: SetupConfiguration, + kind: SetupResourceKind, +): SetupResourceStoragePolicy { + return getSetupStorageConfiguration(configuration)[kind === 'secret' ? 'secrets' : 'variables']; +} + +export function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration { + const fallback = createDefaultSetupStorageConfiguration(); + return { + secrets: mergeStoragePolicy(fallback.secrets, configuration.storage?.secrets), + variables: mergeStoragePolicy(fallback.variables, configuration.storage?.variables), + }; +} + +export function resolveSetupResourceTarget( + configuration: SetupConfiguration, + kind: SetupResourceKind, + name: string, + remote?: SetupRemoteConfiguration, +): SetupResourceTarget { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + const existingScope = setupResourceExists(remote, kind, name).effective; + const scope = existingScope && policy.preserveExisting && !explicitOverride + ? existingScope + : resolveSetupResourceScope(policy, name); + return { + scope, + organizationVisibility: policy.organizationVisibility, + repositoryId: remote?.repositoryId, + }; +} + +export function setupResourceExists( + remote: SetupRemoteConfiguration | undefined, + kind: SetupResourceKind, + name: string, +): { repository: boolean; organization: boolean; effective?: SetupResourceScope } { + if (!remote) return { repository: false, organization: false }; + const repository = kind === 'secret' + ? remote.repositorySecrets.includes(name) + : remote.repositoryVariables.some(variable => variable.name === name); + const organizationAccess = kind === 'secret' + ? (remote.organizationSecretsAccess ?? remote.organizationAccess) + : (remote.organizationVariablesAccess ?? remote.organizationAccess); + const organization = organizationAccess === 'available' && (kind === 'secret' + ? remote.organizationSecrets.includes(name) + : remote.organizationVariables.some(variable => variable.name === name)); + return { + repository, + organization, + effective: repository ? 'repository' : organization ? 'organization' : undefined, + }; +} + +export function shouldUpsertSetupResource( + configuration: SetupConfiguration, + kind: SetupResourceKind, + name: string, + remote?: SetupRemoteConfiguration, +): boolean { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const state = setupResourceExists(remote, kind, name); + if (!state.effective) return true; + const requested = resolveSetupResourceScope(policy, name); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + return requested === state.effective || explicitOverride || !policy.preserveExisting; +} + +export function validateSetupStorageAgainstRemote( + configuration: SetupConfiguration, + remote: SetupRemoteConfiguration, +): string[] { + const errors: string[] = []; + const policies: Array<[SetupResourceKind, SetupResourceStoragePolicy, boolean]> = [ + ['secret', getSetupResourceStoragePolicy(configuration, 'secret'), configuration.manageRepositorySecrets], + ['variable', getSetupResourceStoragePolicy(configuration, 'variable'), configuration.manageRepositoryVariables], + ]; + for (const [kind, policy, managed] of policies) { + if (!managed) continue; + const needsOrganization = policy.defaultScope === 'organization' + || Object.values(policy.overrides).includes('organization'); + if (!needsOrganization) continue; + if (remote.ownerType !== 'Organization') { + errors.push(`Organization-level ${kind} storage is only available for organization-owned repositories.`); + continue; + } + const access = kind === 'secret' ? remote.organizationSecretsAccess : remote.organizationVariablesAccess; + if (access !== 'available') { + errors.push(`The setup PAT cannot inspect organization ${kind}s for this repository. Organization ${kind} permissions are required.`); + } + if (policy.organizationVisibility === 'selected' && remote.repositoryId === undefined) { + errors.push(`The repository ID is required for selected organization ${kind} access.`); + } + } + return errors; +} + +export function usesOrganizationStorage(configuration: SetupConfiguration): boolean { + const storage = getSetupStorageConfiguration(configuration); + return [storage.secrets, storage.variables].some(policy => + policy.defaultScope === 'organization' || Object.values(policy.overrides).includes('organization'), + ); +} + +function mergeStoragePolicy( + base: SetupResourceStoragePolicy | undefined, + override: Partial | undefined, +): SetupResourceStoragePolicy { + const fallback = base ?? defaultStoragePolicy(); + return { + ...fallback, + ...(override ?? {}), + overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, + }; +} + +function validateStorageConfiguration(storage: SetupStorageConfiguration | undefined): string[] { + // Setup files created before scoped storage was introduced remain valid and + // receive the repository-level defaults through getSetupStorageConfiguration. + if (!storage) return []; + const errors: string[] = []; + for (const [kind, policy] of Object.entries(storage)) { + if (!policy || !['repository', 'organization'].includes(policy.defaultScope)) { + errors.push(`${kind} default scope must be repository or organization.`); + continue; + } + if (!['all', 'private', 'selected'].includes(policy.organizationVisibility)) { + errors.push(`${kind} organization visibility must be all, private, or selected.`); + } + if (typeof policy.preserveExisting !== 'boolean') errors.push(`${kind} preserveExisting must be a boolean.`); + for (const [name, scope] of Object.entries(policy.overrides ?? {}) as [string, SetupResourceScope][]) { + if (!RESOURCE_NAME_PATTERN.test(name)) errors.push(`${kind} override name ${name} must be an uppercase GitHub Actions name.`); + if (!['repository', 'organization'].includes(scope)) errors.push(`${kind} override ${name} must use repository or organization.`); + } + } + return errors; +} + function unique(values: string[]): string[] { return [...new Set(values.map(value => value.trim()).filter(Boolean))]; } diff --git a/src/application/ports/setup_wizard_ports.ts b/src/application/ports/setup_wizard_ports.ts index 514b1085..c0612e97 100644 --- a/src/application/ports/setup_wizard_ports.ts +++ b/src/application/ports/setup_wizard_ports.ts @@ -2,11 +2,15 @@ import type { SetupConfiguration, SetupPlan, SetupCredentialCheck, - SetupCredentialRequirement, SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, DoctorCheck, + SetupCredentialRequirement, + SetupResourceTarget, + SetupRemoteConfiguration, + SetupStorageConfiguration, + SetupVariable, } from '../../domain/setup'; export interface SetupPromptPort { @@ -16,6 +20,20 @@ export interface SetupPromptPort { close(): void; } +export interface SetupStoragePromptPort { + chooseStorage( + defaults: SetupStorageConfiguration, + remote: SetupRemoteConfiguration, + variables: readonly SetupVariable[], + requirements: readonly SetupCredentialRequirement[], + managed?: { secrets: boolean; variables: boolean }, + ): Promise; +} + +export interface SetupRemoteConfigurationReadPort { + inspect(owner: string, repository: string, token: string): Promise; +} + export interface SetupCredentialPromptPort { requestSetupPat(): Promise; explainCredentialSeparation(requirements: readonly SetupCredentialRequirement[]): void; @@ -33,6 +51,13 @@ export interface SetupRepositorySecretsPort { token: string, credentials: readonly SetupCredentialValue[], ): Promise<{ created: number; updated: number; skipped: number; errors: string[] }>; + upsertScopedSecrets?( + owner: string, + repository: string, + token: string, + target: SetupResourceTarget, + credentials: readonly SetupCredentialValue[], + ): Promise<{ created: number; updated: number; skipped: number; errors: string[] }>; } export interface SetupRepositoryConfigurationReadPort { @@ -69,4 +94,11 @@ export interface SetupRepositoryVariablesPort { token: string, variables: readonly { name: string; value: string }[], ): Promise<{ created: number; updated: number; errors: string[] }>; + upsertScopedVariables?( + owner: string, + repository: string, + token: string, + target: SetupResourceTarget, + variables: readonly { name: string; value: string }[], + ): Promise<{ created: number; updated: number; errors: string[] }>; } diff --git a/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts b/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts index de9ccc71..44e5f7c9 100644 --- a/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts @@ -182,6 +182,45 @@ describe('InitialSetupUseCase', () => { expect(results[0].steps).toContain('⏭️ Initial version tag creation disabled by setup configuration.'); }); + it('provisions Variables at organization scope when the configuration selects it', async () => { + const setupConfiguration = createDefaultSetupConfiguration(); + setupConfiguration.features.release = false; + setupConfiguration.createInitialTag = false; + setupConfiguration.manageRepositorySecrets = false; + setupConfiguration.storage.variables.defaultScope = 'organization'; + const scopedUpsert = jest.fn().mockResolvedValue({ created: 1, updated: 0, errors: [] }); + const remoteConfiguration = { + ownerType: 'Organization' as const, + repositoryId: 42, + repositoryVisibility: 'private' as const, + repositorySecrets: [], organizationSecrets: [], repositoryVariables: [], organizationVariables: [], + organizationAccess: 'available' as const, organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + const scopedUseCase = new InitialSetupUseCase( + { getUserFromToken: mockGetUserFromToken, getTokenUserDetails: jest.fn() }, + { ensureInitialLabels: mockEnsureInitialLabels }, + { ensureIssueTypes: mockEnsureIssueTypes }, + { getLatestTag: mockGetLatestTag }, + { getDefaultBranch: mockGetDefaultBranch } as any, + { createTag: mockCreateTag } as any, + { prepare: mockSetupPrepare, hasValidToken: mockSetupHasValidToken }, + { upsert: mockSetupVariablesUpsert, upsertScopedVariables: scopedUpsert }, + undefined, + { inspect: jest.fn().mockResolvedValue(remoteConfiguration) }, + ); + + const results = await scopedUseCase.invoke(baseParam({ inputs: { setupConfiguration } })); + + expect(results[0].success).toBe(true); + expect(scopedUpsert).toHaveBeenCalledWith( + 'owner', 'repo', 'token', + expect.objectContaining({ scope: 'organization', repositoryId: 42 }), + expect.arrayContaining([{ name: 'AGENT_PROVIDER', value: 'codex' }]), + ); + expect(mockSetupVariablesUpsert).not.toHaveBeenCalled(); + }); + it('does not create default tag when repository already has tags', async () => { mockGetLatestTag.mockResolvedValue('2.0.0'); const param = baseParam(); diff --git a/src/application/usecases/actions/initial_setup_use_case.ts b/src/application/usecases/actions/initial_setup_use_case.ts index b9e248fe..497d2fed 100644 --- a/src/application/usecases/actions/initial_setup_use_case.ts +++ b/src/application/usecases/actions/initial_setup_use_case.ts @@ -7,7 +7,11 @@ import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '.. import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { ParamUseCase } from '../base/param_usecase'; import { runInitialSetupWorkflow } from './initial_setup_workflow'; -import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +import type { + SetupRemoteConfigurationReadPort, + SetupRepositorySecretsPort, + SetupRepositoryVariablesPort, +} from '../../ports/setup_wizard_ports'; /** Application boundary for provisioning a repository for Copilot automation. */ export class InitialSetupUseCase implements ParamUseCase { @@ -23,6 +27,7 @@ export class InitialSetupUseCase implements ParamUseCase { private readonly setupWorkspacePort: SetupWorkspacePort, private readonly setupRepositoryVariablesPort?: SetupRepositoryVariablesPort, private readonly setupRepositorySecretsPort?: SetupRepositorySecretsPort, + private readonly setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort, ) {} async invoke(param: Execution): Promise { @@ -36,6 +41,7 @@ export class InitialSetupUseCase implements ParamUseCase { setupWorkspacePort: this.setupWorkspacePort, setupRepositoryVariablesPort: this.setupRepositoryVariablesPort, setupRepositorySecretsPort: this.setupRepositorySecretsPort, + setupRemoteConfigurationReadPort: this.setupRemoteConfigurationReadPort, }); } } diff --git a/src/application/usecases/actions/initial_setup_workflow.ts b/src/application/usecases/actions/initial_setup_workflow.ts index 6cb0afaa..df293261 100644 --- a/src/application/usecases/actions/initial_setup_workflow.ts +++ b/src/application/usecases/actions/initial_setup_workflow.ts @@ -12,10 +12,18 @@ import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { DEFAULT_INITIAL_TAG } from '../../../data/model/version_policy'; import { logDebugInfo, logError, logInfo } from '../../ports/logging_ports'; import { getTaskEmoji } from '../../../utils/task_emoji'; -import type { SetupConfiguration } from '../../../domain/setup'; -import type { SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; -import type { SetupCredentialCollection } from '../../../domain/setup'; -import { buildSetupRepositoryVariables } from '../../policies/setup_configuration_policy'; +import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration, SetupResourceTarget } from '../../../domain/setup'; +import type { + SetupRemoteConfigurationReadPort, + SetupRepositorySecretsPort, + SetupRepositoryVariablesPort, +} from '../../ports/setup_wizard_ports'; +import { + buildSetupRepositoryVariables, + resolveSetupResourceTarget, + shouldUpsertSetupResource, + usesOrganizationStorage, +} from '../../policies/setup_configuration_policy'; export interface InitialSetupWorkflowDependencies { authenticatedUserPort: AuthenticatedUserPort; @@ -27,6 +35,7 @@ export interface InitialSetupWorkflowDependencies { setupWorkspacePort: SetupWorkspacePort; setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; setupRepositorySecretsPort?: SetupRepositorySecretsPort; + setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; } type InitialLabelProvisioningOutcome = @@ -70,7 +79,9 @@ export async function runInitialSetupWorkflow( } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); - const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration); + const remoteConfiguration = await resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors); + + const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration); if (secrets.step) steps.push(secrets.step); if (secrets.errors.length > 0) errors.push(...secrets.errors); @@ -91,7 +102,7 @@ export async function runInitialSetupWorkflow( steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration); + const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration); if (variables.step) steps.push(variables.step); if (variables.errors.length > 0) errors.push(...variables.errors); @@ -220,20 +231,18 @@ async function ensureRepositoryVariables( param: Execution, dependencies: InitialSetupWorkflowDependencies, setupConfiguration?: SetupConfiguration, + remoteConfiguration?: SetupRemoteConfiguration, ): Promise<{ step?: string; errors: string[] }> { if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { return { errors: [] }; } try { - const result = await dependencies.setupRepositoryVariablesPort.upsert( - param.owner, - param.repo, - param.tokens.token, - buildSetupRepositoryVariables(setupConfiguration), - ); + const desired = buildSetupRepositoryVariables(setupConfiguration); + const groups = groupResources(desired, 'variable', setupConfiguration, remoteConfiguration); + const result = await upsertVariableGroups(param, dependencies.setupRepositoryVariablesPort, groups); if (result.errors.length > 0) return { errors: result.errors }; return { - step: `✅ Repository Variables: ${result.created} created, ${result.updated} updated`, + step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, errors: [], }; } catch (error) { @@ -247,6 +256,7 @@ async function ensureRepositorySecrets( param: Execution, dependencies: InitialSetupWorkflowDependencies, setupConfiguration?: SetupConfiguration, + remoteConfiguration?: SetupRemoteConfiguration, ): Promise<{ step?: string; errors: string[] }> { if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { return { errors: [] }; @@ -261,15 +271,11 @@ async function ensureRepositorySecrets( ]; if (values.length === 0) return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; try { - const result = await dependencies.setupRepositorySecretsPort.upsertSecrets( - param.owner, - param.repo, - param.tokens.token, - values, - ); + const groups = groupResources(values, 'secret', setupConfiguration, remoteConfiguration); + const result = await upsertSecretGroups(param, dependencies.setupRepositorySecretsPort, groups); if (result.errors.length > 0) return { errors: result.errors }; return { - step: `✅ Repository Secrets: ${result.created} created, ${result.updated} updated; existing values kept when no replacement was selected.`, + step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, errors: [], }; } catch (error) { @@ -279,6 +285,97 @@ async function ensureRepositorySecrets( } } +async function resolveRemoteConfiguration( + param: Execution, + dependencies: InitialSetupWorkflowDependencies, + setupConfiguration: SetupConfiguration | undefined, + errors: string[], +): Promise { + const provided = param.inputs?.setupRemoteConfiguration; + if (provided && typeof provided === 'object') return provided as SetupRemoteConfiguration; + if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) return undefined; + try { + return await dependencies.setupRemoteConfigurationReadPort.inspect(param.owner, param.repo, param.tokens.token); + } catch (error) { + const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + logError(message); + if (usesOrganizationStorage(setupConfiguration)) errors.push(message); + return undefined; + } +} + +type SetupResource = { name: string; value: string }; +type ResourceGroup = { target: SetupResourceTarget; resources: SetupResource[] }; + +function groupResources( + resources: readonly SetupResource[], + kind: 'secret' | 'variable', + configuration: SetupConfiguration, + remoteConfiguration?: SetupRemoteConfiguration, +): ResourceGroup[] { + const groups = new Map(); + for (const resource of resources) { + // Secret values reach this workflow only after the user chose keep/replace. + // Variables, however, are always generated from the selected setup contract, + // so preserveExisting must be applied here to avoid shadowing inherited values. + if (kind === 'variable' && !shouldUpsertSetupResource(configuration, kind, resource.name, remoteConfiguration)) continue; + const target = resolveSetupResourceTarget(configuration, kind, resource.name, remoteConfiguration); + const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; + const group = groups.get(key) ?? { target, resources: [] }; + group.resources.push(resource); + groups.set(key, group); + } + return [...groups.values()]; +} + +async function upsertVariableGroups( + param: Execution, + port: SetupRepositoryVariablesPort, + groups: readonly ResourceGroup[], +): Promise<{ created: number; updated: number; errors: string[] }> { + let created = 0; + let updated = 0; + const errors: string[] = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedVariables) { + errors.push('Organization Variable provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedVariables!(param.owner, param.repo, param.tokens.token, group.target, group.resources) + : await port.upsert(param.owner, param.repo, param.tokens.token, group.resources); + created += result.created; + updated += result.updated; + errors.push(...result.errors); + } + return { created, updated, errors }; +} + +async function upsertSecretGroups( + param: Execution, + port: SetupRepositorySecretsPort, + groups: readonly ResourceGroup[], +): Promise<{ created: number; updated: number; skipped: number; errors: string[] }> { + let created = 0; + let updated = 0; + let skipped = 0; + const errors: string[] = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { + errors.push('Organization Secret provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedSecrets!(param.owner, param.repo, param.tokens.token, group.target, group.resources) + : await port.upsertSecrets(param.owner, param.repo, param.tokens.token, group.resources); + created += result.created; + updated += result.updated; + skipped += result.skipped; + errors.push(...result.errors); + } + return { created, updated, skipped, errors }; +} + function getSetupCredentialCollection(param: Execution): SetupCredentialCollection | undefined { const credentials = param.inputs?.setupCredentials; if (!credentials || typeof credentials !== 'object') return undefined; diff --git a/src/application/usecases/setup/__tests__/doctor_use_case.test.ts b/src/application/usecases/setup/__tests__/doctor_use_case.test.ts index 60c56fe3..9cd479d7 100644 --- a/src/application/usecases/setup/__tests__/doctor_use_case.test.ts +++ b/src/application/usecases/setup/__tests__/doctor_use_case.test.ts @@ -83,4 +83,69 @@ describe('SetupDoctorUseCase', () => { expect.objectContaining({ area: 'Variable AGENT_PROVIDER', status: 'fail' }), ])); }); + + it('uses organization resources as effective values and reports their scope', async () => { + const configuration = createDefaultSetupConfiguration(); + const requiredSecrets = buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); + const requiredVariables = buildSetupRepositoryVariables(configuration); + const { output, dependencies } = createDependencies({ + variables: { listVariables: jest.fn() }, + secrets: { list: jest.fn(), upsertSecrets: jest.fn() }, + }); + const remoteHealth = { + validateExisting: jest.fn().mockResolvedValue(requiredSecrets.map(name => ({ name, status: 'valid', message: 'remote ok' }))), + }; + const remoteConfiguration = { + ownerType: 'Organization' as const, + repositoryId: 42, + repositoryVisibility: 'private' as const, + repositorySecrets: [], + organizationSecrets: requiredSecrets, + repositoryVariables: [], + organizationVariables: requiredVariables, + organizationAccess: 'available' as const, + organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + const reader = { inspect: jest.fn().mockResolvedValue(remoteConfiguration) }; + + const healthy = await new SetupDoctorUseCase( + dependencies.validation, + dependencies.secrets, + dependencies.variables, + dependencies.workspace, + output, + remoteHealth, + reader, + ).execute({ owner: 'owner', repository: 'repo', setupToken: 'token', configuration }); + + expect(healthy).toBe(true); + expect(dependencies.variables.listVariables).not.toHaveBeenCalled(); + expect(dependencies.secrets.list).not.toHaveBeenCalled(); + expect(output.showDoctorChecks).toHaveBeenCalledWith(expect.arrayContaining([ + expect.objectContaining({ area: 'Variable AGENT_PROVIDER', message: expect.stringContaining('organization scope') }), + ])); + }); + + it('fails organization-scoped doctor checks when remote scope inspection is unavailable', async () => { + const configuration = createDefaultSetupConfiguration(); + configuration.storage.variables.defaultScope = 'organization'; + const { output, dependencies } = createDependencies(); + const reader = { inspect: jest.fn().mockRejectedValue(new Error('forbidden')) }; + + const healthy = await new SetupDoctorUseCase( + dependencies.validation, + dependencies.secrets, + dependencies.variables, + dependencies.workspace, + output, + undefined, + reader, + ).execute({ owner: 'owner', repository: 'repo', setupToken: 'token', configuration }); + + expect(healthy).toBe(false); + expect(output.showDoctorChecks).toHaveBeenCalledWith(expect.arrayContaining([ + expect.objectContaining({ area: 'GitHub Actions scopes', status: 'fail' }), + ])); + }); }); diff --git a/src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts b/src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts index 8035426b..2f3a7fad 100644 --- a/src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts +++ b/src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts @@ -72,4 +72,30 @@ describe('SetupCredentialsUseCase', () => { })).rejects.toThrow('OPENAI_API_KEY is invalid and must be replaced'); expect(prompt.requestApiKey).not.toHaveBeenCalled(); }); + + it('detects an existing organization Secret and keeps it without requesting its value', async () => { + const prompt = { + requestSetupPat: jest.fn(), explainCredentialSeparation: jest.fn(), requestWorkflowPat: jest.fn(), requestApiKey: jest.fn(), + chooseExistingCredential: jest.fn().mockResolvedValue('keep'), showCredentialChecks: jest.fn(), + }; + const validation = { validateSetupPat: jest.fn().mockResolvedValue({ name: 'SETUP_PAT', status: 'valid', message: 'ok' }), validateCredential: jest.fn() }; + const secrets = { list: jest.fn(), upsertSecrets: jest.fn() }; + const remoteHealth = { validateExisting: jest.fn().mockResolvedValue([{ name: 'PAT', status: 'valid', message: 'remote ok' }]) }; + const remoteConfiguration = { + ownerType: 'Organization' as const, repositoryId: 42, repositoryVisibility: 'private' as const, + repositorySecrets: [], organizationSecrets: ['PAT'], repositoryVariables: [], organizationVariables: [], + organizationAccess: 'available' as const, organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + + const result = await new SetupCredentialsUseCase(prompt, validation, secrets, remoteHealth).collect({ + owner: 'owner', repository: 'repo', setupToken: 'setup-token', ref: 'main', + requirements: [requirement('PAT', 'workflowPat')], manageSecrets: true, remoteConfiguration, + }); + + expect(result.collection).toEqual({ apiKeys: [] }); + expect(prompt.requestWorkflowPat).not.toHaveBeenCalled(); + expect(prompt.chooseExistingCredential).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ sourceScope: 'organization' })); + expect(secrets.list).not.toHaveBeenCalled(); + }); }); diff --git a/src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts b/src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts index 6cec2f56..9e8cee86 100644 --- a/src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts +++ b/src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts @@ -1,6 +1,7 @@ import { SetupWizardUseCase } from '../setup_wizard_use_case'; import { createDefaultSetupConfiguration } from '../../../policies/setup_configuration_policy'; -import type { SetupPromptPort } from '../../../ports/setup_wizard_ports'; +import type { SetupPromptPort, SetupRemoteConfigurationReadPort, SetupStoragePromptPort } from '../../../ports/setup_wizard_ports'; +import type { SetupCredentialRequirement, SetupRemoteConfiguration, SetupStorageConfiguration, SetupVariable } from '../../../../domain/setup'; describe('SetupWizardUseCase', () => { it('validates, previews, and confirms the collected configuration', async () => { @@ -42,4 +43,44 @@ describe('SetupWizardUseCase', () => { await expect(new SetupWizardUseCase(prompt).collect()).resolves.toBeUndefined(); }); + + it('inspects the remote repository and collects independent storage decisions', async () => { + const prompt: jest.Mocked = { + collect: jest.fn(async defaults => defaults), + showPlan: jest.fn(), confirm: jest.fn(async (_plan) => true), close: jest.fn(), + }; + const remote = { + ownerType: 'Organization' as const, repositoryId: 7, repositoryVisibility: 'private' as const, + repositorySecrets: [], organizationSecrets: ['PAT'], repositoryVariables: [], + organizationVariables: [{ name: 'AGENT_PROVIDER', value: 'codex' }], + organizationAccess: 'available' as const, organizationSecretsAccess: 'available' as const, + organizationVariablesAccess: 'available' as const, + }; + const reader: jest.Mocked = { inspect: jest.fn().mockResolvedValue(remote) }; + const storagePrompt: jest.Mocked = { + chooseStorage: jest.fn(async ( + defaults: SetupStorageConfiguration, + _remote: SetupRemoteConfiguration, + _variables: readonly SetupVariable[], + _requirements: readonly SetupCredentialRequirement[], + _managed?: { secrets: boolean; variables: boolean }, + ): Promise => ({ + ...defaults, + secrets: { ...defaults.secrets, defaultScope: 'organization' as const }, + variables: { ...defaults.variables, defaultScope: 'organization' as const }, + })), + }; + + const result = await new SetupWizardUseCase(prompt, reader, storagePrompt).collect({ + remoteTarget: { owner: 'owner', repository: 'repo', token: 'token' }, + }); + + expect(result?.storage.secrets.defaultScope).toBe('organization'); + expect(result?.storage.variables.defaultScope).toBe('organization'); + expect(reader.inspect).toHaveBeenCalledWith('owner', 'repo', 'token'); + expect(storagePrompt.chooseStorage).toHaveBeenCalledWith( + expect.anything(), remote, expect.any(Array), expect.any(Array), + { secrets: true, variables: true }, + ); + }); }); diff --git a/src/application/usecases/setup/doctor_use_case.ts b/src/application/usecases/setup/doctor_use_case.ts index d8ed143d..289cecf5 100644 --- a/src/application/usecases/setup/doctor_use_case.ts +++ b/src/application/usecases/setup/doctor_use_case.ts @@ -1,10 +1,18 @@ -import type { SetupConfiguration, DoctorCheck } from '../../../domain/setup'; -import { buildSetupCredentialRequirements, buildSetupRepositoryVariables } from '../../policies/setup_configuration_policy'; +import type { SetupConfiguration, DoctorCheck, SetupRemoteConfiguration } from '../../../domain/setup'; +import { + buildSetupCredentialRequirements, + buildSetupRepositoryVariables, + getSetupResourceStoragePolicy, + resolveSetupResourceScope, + setupResourceExists, + usesOrganizationStorage, +} from '../../policies/setup_configuration_policy'; import type { DoctorOutputPort, SetupCredentialValidationPort, SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, + SetupRemoteConfigurationReadPort, SetupRemoteCredentialHealthPort, } from '../../ports/setup_wizard_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; @@ -24,6 +32,7 @@ export class SetupDoctorUseCase { private readonly workspace: SetupWorkspacePort, private readonly output: DoctorOutputPort, private readonly remoteHealth?: SetupRemoteCredentialHealthPort, + private readonly remoteConfigurationReader?: SetupRemoteConfigurationReadPort, ) {} async execute(request: DoctorRequest): Promise { @@ -44,19 +53,71 @@ export class SetupDoctorUseCase { }); } + let remoteConfiguration: SetupRemoteConfiguration | undefined; + if (this.remoteConfigurationReader) { + try { + remoteConfiguration = await this.remoteConfigurationReader.inspect( + request.owner, + request.repository, + request.setupToken, + ); + } catch (error) { + const message = `Could not inspect GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + checks.push({ + area: 'GitHub Actions scopes', + status: usesOrganizationStorage(request.configuration) ? 'fail' : 'warn', + message, + }); + } + } + const requiredVariables = buildSetupRepositoryVariables(request.configuration); - const remoteVariables = await this.variables.listVariables(request.owner, request.repository, request.setupToken); - const remoteVariableMap = new Map(remoteVariables.map(variable => [variable.name, variable.value])); + const remoteVariables = remoteConfiguration?.repositoryVariables + ?? await this.variables.listVariables(request.owner, request.repository, request.setupToken); + const remoteVariableMap = new Map( + remoteVariables.map(variable => [variable.name, { value: variable.value, source: 'repository' as const }]), + ); + if (remoteConfiguration) { + for (const variable of remoteConfiguration.organizationVariables) { + if (!remoteVariableMap.has(variable.name)) { + remoteVariableMap.set(variable.name, { value: variable.value, source: 'organization' as const }); + } + } + } for (const variable of requiredVariables) { - const value = remoteVariableMap.get(variable.name); + const remoteVariable = remoteVariableMap.get(variable.name); + const value = remoteVariable?.value; + const state = setupResourceExists(remoteConfiguration, 'variable', variable.name); + const policy = getSetupResourceStoragePolicy(request.configuration, 'variable'); + const preserveExisting = state.effective !== undefined + && state.effective !== resolveSetupResourceScope(policy, variable.name) + && !Object.prototype.hasOwnProperty.call(policy.overrides, variable.name) + && policy.preserveExisting; + const sourceMessage = remoteVariable?.source === 'organization' + ? ' Variable is inherited from the organization scope.' + : remoteVariable + ? ' Variable is configured at repository scope.' + : ''; + const matches = value === variable.value; checks.push({ area: `Variable ${variable.name}`, - status: value === undefined ? 'fail' : value === variable.value ? 'pass' : 'fail', - message: value === undefined ? 'Variable is missing.' : value === variable.value ? 'Variable is configured.' : 'Variable exists but differs from the selected setup configuration.', + status: value === undefined ? 'fail' : matches ? 'pass' : preserveExisting ? 'warn' : 'fail', + message: value === undefined + ? 'Variable is missing.' + : matches + ? `Variable is configured.${sourceMessage}` + : preserveExisting + ? `Variable differs from the selected setup configuration but is preserved at ${remoteVariable?.source} scope.` + : 'Variable exists but differs from the selected setup configuration.', }); } - const remoteSecrets = new Set(await this.secrets.list(request.owner, request.repository, request.setupToken)); + const repositorySecretNames = remoteConfiguration?.repositorySecrets + ?? await this.secrets.list(request.owner, request.repository, request.setupToken); + const remoteSecrets = new Set(repositorySecretNames); + if (remoteConfiguration) { + for (const secret of remoteConfiguration.organizationSecrets) remoteSecrets.add(secret); + } const requirements = buildSetupCredentialRequirements(request.configuration); const remoteHealth = this.remoteHealth ? await this.remoteHealth.validateExisting(request.owner, request.repository, request.setupToken, request.configuration.repository.mainBranch, requirements.filter(requirement => remoteSecrets.has(requirement.name))) diff --git a/src/application/usecases/setup/setup_credentials_use_case.ts b/src/application/usecases/setup/setup_credentials_use_case.ts index d2bb498b..03e75148 100644 --- a/src/application/usecases/setup/setup_credentials_use_case.ts +++ b/src/application/usecases/setup/setup_credentials_use_case.ts @@ -11,6 +11,7 @@ import type { SetupRemoteCredentialHealthPort, } from '../../ports/setup_wizard_ports'; import { ApplicationError } from '../../errors/application_error'; +import type { SetupRemoteConfiguration, SetupResourceScope } from '../../../domain/setup'; export interface SetupCredentialsRequest { owner: string; @@ -19,6 +20,7 @@ export interface SetupCredentialsRequest { requirements: readonly SetupCredentialRequirement[]; manageSecrets: boolean; ref?: string; + remoteConfiguration?: SetupRemoteConfiguration; } export interface SetupCredentialsResult { @@ -47,10 +49,15 @@ export class SetupCredentialsUseCase { } if (!this.secrets) throw new ApplicationError('Repository Secret provisioning is not available in this installation.', 'configuration'); - const existingSecretNames = await this.secrets.list(request.owner, request.repository, request.setupToken); + const existingSecretNames = request.remoteConfiguration?.repositorySecrets + ? [...request.remoteConfiguration.repositorySecrets] + : await this.secrets.list(request.owner, request.repository, request.setupToken); + const existingOrganizationSecretNames = request.remoteConfiguration?.organizationSecrets ?? []; const requirements = request.requirements.filter(requirement => requirement.name !== 'SETUP_PAT'); this.prompt.explainCredentialSeparation(requirements); - const existingRequirements = requirements.filter(requirement => existingSecretNames.includes(requirement.name)); + const existingRequirements = requirements.filter(requirement => + existingSecretNames.includes(requirement.name) || existingOrganizationSecretNames.includes(requirement.name), + ); const remoteChecks = this.remoteHealth && existingRequirements.length > 0 ? await this.remoteHealth.validateExisting( request.owner, @@ -65,15 +72,23 @@ export class SetupCredentialsUseCase { const values: SetupCredentialValue[] = []; for (const requirement of requirements) { - const existing = existingSecretNames.includes(requirement.name); + const repositoryExisting = existingSecretNames.includes(requirement.name); + const organizationExisting = existingOrganizationSecretNames.includes(requirement.name); + const existing = repositoryExisting || organizationExisting; + const sourceScope: SetupResourceScope | undefined = repositoryExisting + ? 'repository' + : organizationExisting + ? 'organization' + : undefined; if (existing) { const remoteCheck: SetupCredentialCheck = remoteCheckByName.get(requirement.name) ?? { name: requirement.name, status: 'unverifiable', message: 'The remote health workflow is not available yet; GitHub does not reveal Secret values.', }; - checks.push(remoteCheck); - const decision = await this.prompt.chooseExistingCredential(requirement, remoteCheck); + const scopedCheck = { ...remoteCheck, sourceScope }; + checks.push(scopedCheck); + const decision = await this.prompt.chooseExistingCredential(requirement, scopedCheck); if (remoteCheck.status === 'invalid' && decision !== 'replace') { throw new ApplicationError(`${requirement.name} is invalid and must be replaced before setup can continue.`, 'authorization'); } diff --git a/src/application/usecases/setup/setup_wizard_use_case.ts b/src/application/usecases/setup/setup_wizard_use_case.ts index 8d66d0e9..8605ee4b 100644 --- a/src/application/usecases/setup/setup_wizard_use_case.ts +++ b/src/application/usecases/setup/setup_wizard_use_case.ts @@ -1,10 +1,18 @@ -import type { SetupPromptPort } from '../../ports/setup_wizard_ports'; +import type { + SetupPromptPort, + SetupRemoteConfigurationReadPort, + SetupStoragePromptPort, +} from '../../ports/setup_wizard_ports'; import { ApplicationError } from '../../errors/application_error'; -import type { SetupConfiguration, SetupPlan } from '../../../domain/setup'; +import type { SetupConfiguration, SetupPlan, SetupRemoteConfiguration } from '../../../domain/setup'; import { + buildSetupCredentialRequirements, + buildSetupRepositoryVariables, buildSetupPlan, createDefaultSetupConfiguration, + getSetupStorageConfiguration, mergeSetupConfiguration, + validateSetupStorageAgainstRemote, validateSetupConfiguration, type SetupConfigurationOverrides, } from '../../policies/setup_configuration_policy'; @@ -12,23 +20,65 @@ import { export interface SetupWizardRequest { overrides?: SetupConfigurationOverrides; skipRepositoryVariables?: boolean; + skipRepositorySecrets?: boolean; + remoteTarget?: { + owner: string; + repository: string; + token: string; + }; } export class SetupWizardUseCase { - constructor(private readonly prompt: SetupPromptPort) {} + private lastRemoteConfiguration: SetupRemoteConfiguration | undefined; + + constructor( + private readonly prompt: SetupPromptPort, + private readonly remoteConfigurationReader?: SetupRemoteConfigurationReadPort, + private readonly storagePrompt?: SetupStoragePromptPort, + ) {} async collect(request: SetupWizardRequest = {}): Promise { + this.lastRemoteConfiguration = undefined; const defaults = mergeSetupConfiguration( createDefaultSetupConfiguration(), { ...request.overrides, ...(request.skipRepositoryVariables ? { manageRepositoryVariables: false } : {}), + ...(request.skipRepositorySecrets ? { manageRepositorySecrets: false } : {}), }, ); const collected = await this.prompt.collect(defaults); - const configuration = request.skipRepositoryVariables - ? { ...collected, manageRepositoryVariables: false } - : collected; + let configuration = { + ...collected, + ...(request.skipRepositoryVariables ? { manageRepositoryVariables: false } : {}), + ...(request.skipRepositorySecrets ? { manageRepositorySecrets: false } : {}), + }; + if (request.remoteTarget && this.remoteConfigurationReader && this.storagePrompt) { + const remote = await this.remoteConfigurationReader.inspect( + request.remoteTarget.owner, + request.remoteTarget.repository, + request.remoteTarget.token, + ); + this.lastRemoteConfiguration = remote; + const storage = await this.storagePrompt.chooseStorage( + getSetupStorageConfiguration(configuration), + remote, + buildSetupRepositoryVariables(configuration), + buildSetupCredentialRequirements(configuration), + { + secrets: configuration.manageRepositorySecrets, + variables: configuration.manageRepositoryVariables, + }, + ); + configuration = { ...configuration, storage }; + const remoteErrors = validateSetupStorageAgainstRemote(configuration, remote); + if (remoteErrors.length > 0) { + throw new ApplicationError( + `Invalid remote storage configuration:\n${remoteErrors.map(error => `- ${error}`).join('\n')}`, + 'authorization', + ); + } + } const validationErrors = validateSetupConfiguration(configuration); if (validationErrors.length > 0) { throw new ApplicationError( @@ -46,6 +96,10 @@ export class SetupWizardUseCase { return buildSetupPlan(configuration); } + remoteConfiguration(): SetupRemoteConfiguration | undefined { + return this.lastRemoteConfiguration; + } + close(): void { this.prompt.close(); } diff --git a/src/cli/__tests__/setup_config_file.test.ts b/src/cli/__tests__/setup_config_file.test.ts index 01709bef..b594ec2e 100644 --- a/src/cli/__tests__/setup_config_file.test.ts +++ b/src/cli/__tests__/setup_config_file.test.ts @@ -63,6 +63,28 @@ describe('setup configuration file loader', () => { expect(loadSetupConfigurationOverrides(file)).toEqual({ createInitialTag: false }); }); + it('accepts independent organization storage policies without credential values', () => { + const file = join(directory, 'scoped-setup.yml'); + writeFileSync(file, [ + 'storage:', + ' secrets:', + ' defaultScope: organization', + ' organizationVisibility: selected', + ' preserveExisting: true', + ' variables:', + ' defaultScope: repository', + ' overrides:', + ' OPENAI_API_KEY: organization', + ].join('\n')); + + expect(loadSetupConfigurationOverrides(file)).toEqual({ + storage: { + secrets: { defaultScope: 'organization', organizationVisibility: 'selected', preserveExisting: true }, + variables: { defaultScope: 'repository', overrides: { OPENAI_API_KEY: 'organization' } }, + }, + }); + }); + it.each([ ['a secret-like value', '{"actionInputs":{"token":"secret"}}', /must not contain secrets/], ['an unknown field', '{"reposotory":{"mainBranch":"main"}}', /Unknown setup configuration field/], diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index d613a5f9..1495d470 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -9,8 +9,9 @@ import { loadSetupConfigurationOverrides } from '../setup_config_file'; import { SetupWizardUseCase } from '../../application/usecases/setup'; import { SETUP_FEATURE_DESCRIPTIONS, buildSetupCredentialRequirements } from '../../application/policies/setup_configuration_policy'; import type { SetupConfigurationOverrides } from '../../application/policies/setup_configuration_policy'; -import { createSetupCredentialsUseCase } from '../../infrastructure/composition/setup_credentials_composition_root'; +import { createSetupCredentialsUseCase, createSetupRemoteConfigurationReadPort } from '../../infrastructure/composition/setup_credentials_composition_root'; import { SetupWorkspaceAdapter } from '../../infrastructure/setup_workspace_adapter'; +import type { SetupResourceScope } from '../../domain/setup'; export function registerSetupCommand(program: Command): void { program @@ -26,6 +27,12 @@ export function registerSetupCommand(program: Command): void { .option('--dry-run', 'Show the setup plan without changing files or GitHub', false) .option('--skip-variables', 'Do not create or update GitHub Repository Variables', false) .option('--skip-secrets', 'Do not validate or create/update GitHub Repository Secrets', false) + .option('--variables-scope ', 'Default Variable scope (repository|organization)') + .option('--secrets-scope ', 'Default Secret scope (repository|organization)') + .option('--variables-visibility ', 'Organization Variable visibility (selected|private|all)') + .option('--secrets-visibility ', 'Organization Secret visibility (selected|private|all)') + .option('--variable-scope ', 'Per-variable scope override; repeat as needed', collectScope, {}) + .option('--secret-scope ', 'Per-secret scope override; repeat as needed', collectScope, {}) .option('--update-workflows', 'Allow setup-managed workflows already in the repository to be updated', false) .option('--workflow-pat ', 'Workflow PAT for the bot account (prefer the hidden interactive prompt)') .option('--secret ', 'Secret value for non-interactive setup; repeat for each API key', collectSecret, {}) @@ -67,11 +74,16 @@ export function registerSetupCommand(program: Command): void { return; } logInfo(options.dryRun ? '🧭 Building a dry-run setup plan...' : '🧭 Building your setup plan...'); - const wizard = new SetupWizardUseCase(prompt); + const remoteConfigurationReader = typeof createSetupRemoteConfigurationReadPort === 'function' + ? createSetupRemoteConfigurationReadPort() + : undefined; + const wizard = new SetupWizardUseCase(prompt, remoteConfigurationReader, prompt); const overrides = loadSetupOverrides(options); const configuration = await wizard.collect({ overrides, skipRepositoryVariables: Boolean(options.skipVariables), + skipRepositorySecrets: Boolean(options.skipSecrets), + ...(token ? { remoteTarget: { owner: gitInfo.owner, repository: gitInfo.repo, token } } : {}), }); if (!configuration) { logInfo('⏭️ Setup cancelled. No changes were applied.'); @@ -93,9 +105,18 @@ export function registerSetupCommand(program: Command): void { requirements: buildSetupCredentialRequirements(configuration), manageSecrets: !options.skipSecrets && configuration.manageRepositorySecrets, ref: configuration.repository.mainBranch, + remoteConfiguration: wizard.remoteConfiguration(), }); logInfo('⚙️ Applying the approved setup plan...'); - const params = buildSetupParams(options, gitInfo, token ?? '', configuration, credentials.collection, approvedWorkflowFiles); + const params = buildSetupParams( + options, + gitInfo, + token ?? '', + configuration, + credentials.collection, + approvedWorkflowFiles, + wizard.remoteConfiguration(), + ); if (!params) return; await runLocalAction(params); } catch (error) { @@ -120,6 +141,12 @@ function loadSetupOverrides(options: { config?: string; agent?: string; features?: string; + variablesScope?: string; + secretsScope?: string; + variablesVisibility?: string; + secretsVisibility?: string; + variableScope?: Record; + secretScope?: Record; }): SetupConfigurationOverrides { const fromFile = options.config ? loadSetupConfigurationOverrides(options.config) : {}; const fromFlags: SetupConfigurationOverrides = {}; @@ -141,6 +168,22 @@ function loadSetupOverrides(options: { fromFlags.features = Object.fromEntries(Object.keys(SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, requested.includes(feature)])); } } + const storage: NonNullable = {}; + if (options.variablesScope || options.variablesVisibility || Object.keys(options.variableScope ?? {}).length > 0) { + storage.variables = { + ...(options.variablesScope ? { defaultScope: parseScope(options.variablesScope, '--variables-scope') } : {}), + ...(options.variablesVisibility ? { organizationVisibility: parseVisibility(options.variablesVisibility, '--variables-visibility') } : {}), + ...(Object.keys(options.variableScope ?? {}).length > 0 ? { overrides: options.variableScope } : {}), + }; + } + if (options.secretsScope || options.secretsVisibility || Object.keys(options.secretScope ?? {}).length > 0) { + storage.secrets = { + ...(options.secretsScope ? { defaultScope: parseScope(options.secretsScope, '--secrets-scope') } : {}), + ...(options.secretsVisibility ? { organizationVisibility: parseVisibility(options.secretsVisibility, '--secrets-visibility') } : {}), + ...(Object.keys(options.secretScope ?? {}).length > 0 ? { overrides: options.secretScope } : {}), + }; + } + if (Object.keys(storage).length > 0) fromFlags.storage = storage; return mergeSetupOverrides(fromFile, fromFlags); } @@ -156,5 +199,34 @@ function mergeSetupOverrides( repository: { ...fileOverrides.repository, ...flagOverrides.repository }, ai: { ...fileOverrides.ai, ...flagOverrides.ai }, projects: { ...fileOverrides.projects, ...flagOverrides.projects }, + storage: { + ...fileOverrides.storage, + ...flagOverrides.storage, + secrets: { ...fileOverrides.storage?.secrets, ...flagOverrides.storage?.secrets, overrides: { ...fileOverrides.storage?.secrets?.overrides, ...flagOverrides.storage?.secrets?.overrides } }, + variables: { ...fileOverrides.storage?.variables, ...flagOverrides.storage?.variables, overrides: { ...fileOverrides.storage?.variables?.overrides, ...flagOverrides.storage?.variables?.overrides } }, + }, }; } + +function collectScope(value: string, previous: Record): Record { + const separator = value.indexOf('='); + if (separator <= 0) throw new Error('Scope overrides must use NAME=repository or NAME=organization syntax.'); + const name = value.slice(0, separator).trim(); + const scope = value.slice(separator + 1).trim().toLowerCase(); + if (!/^[A-Z][A-Z0-9_]*$/.test(name) || !['repository', 'organization'].includes(scope)) { + throw new Error('Scope overrides must use an uppercase NAME and repository or organization scope.'); + } + return { ...previous, [name]: scope as SetupResourceScope }; +} + +function parseScope(value: string, flag: string): 'repository' | 'organization' { + const normalized = value.trim().toLowerCase(); + if (normalized !== 'repository' && normalized !== 'organization') throw new Error(`${flag} must be repository or organization.`); + return normalized; +} + +function parseVisibility(value: string, flag: string): 'all' | 'private' | 'selected' { + const normalized = value.trim().toLowerCase(); + if (!['all', 'private', 'selected'].includes(normalized)) throw new Error(`${flag} must be selected, private, or all.`); + return normalized as 'all' | 'private' | 'selected'; +} diff --git a/src/cli/commands/setup_policy.ts b/src/cli/commands/setup_policy.ts index c9874b72..3ce7333c 100644 --- a/src/cli/commands/setup_policy.ts +++ b/src/cli/commands/setup_policy.ts @@ -1,6 +1,6 @@ import { ACTIONS, INPUT_KEYS } from '../../utils/constants'; import type { GitInfo } from '../../cli_context'; -import type { SetupConfiguration, SetupCredentialCollection } from '../../domain/setup'; +import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration } from '../../domain/setup'; import { buildSetupActionInputs } from '../../application/policies/setup_configuration_policy'; export interface SetupCommandOptions { @@ -14,6 +14,7 @@ export function buildSetupParams( configuration?: SetupConfiguration, credentials?: SetupCredentialCollection, approvedWorkflowFiles: readonly string[] = [], + remoteConfiguration?: SetupRemoteConfiguration, ): Record | undefined { if ('error' in gitInfo) return undefined; return { @@ -31,6 +32,7 @@ export function buildSetupParams( ], ...(configuration ? { setupConfiguration: configuration } : {}), ...(credentials ? { setupCredentials: credentials } : {}), + ...(remoteConfiguration ? { setupRemoteConfiguration: remoteConfiguration } : {}), setupWorkflowUpdates: approvedWorkflowFiles, }; } diff --git a/src/cli/setup_config_file.ts b/src/cli/setup_config_file.ts index c914aa0e..3c921bb0 100644 --- a/src/cli/setup_config_file.ts +++ b/src/cli/setup_config_file.ts @@ -16,6 +16,7 @@ const SETUP_OVERRIDE_KEYS = new Set([ 'manageRepositoryVariables', 'manageRepositorySecrets', 'actionInputs', + 'storage', ]); const AGENT_OVERRIDE_KEYS = new Set(['provider', 'modelProvider', 'model', 'effort']); const REPOSITORY_STRING_KEYS = new Set([ @@ -43,6 +44,8 @@ const PROJECT_KEYS = new Set([ 'issueInProgressColumn', 'pullRequestInProgressColumn', ]); +const STORAGE_KEYS = new Set(['secrets', 'variables']); +const STORAGE_POLICY_KEYS = new Set(['defaultScope', 'organizationVisibility', 'preserveExisting', 'overrides']); /** Loads a non-secret setup override file. JSON and YAML are supported. */ export function loadSetupConfigurationOverrides(filePath: string): SetupConfigurationOverrides { @@ -79,9 +82,44 @@ export function loadSetupConfigurationOverrides(filePath: string): SetupConfigur validateBooleanProperty(raw, 'manageRepositorySecrets'); validateOptionalObject(raw.actionInputs, 'actionInputs'); if (raw.actionInputs !== undefined) validateStringValues(raw.actionInputs as Record, 'actionInputs'); + validateStorage(raw.storage); return raw as SetupConfigurationOverrides; } +function validateStorage(value: unknown): void { + if (value === undefined) return; + validateObject(value, 'storage'); + const storage = value as Record; + validateObjectKeys(storage, STORAGE_KEYS, 'storage'); + for (const kind of STORAGE_KEYS) { + if (storage[kind] === undefined) continue; + validateObject(storage[kind], `storage.${kind}`); + const policy = storage[kind] as Record; + validateObjectKeys(policy, STORAGE_POLICY_KEYS, `storage.${kind}`); + if (policy.defaultScope !== undefined && !['repository', 'organization'].includes(String(policy.defaultScope))) { + throw new Error(`storage.${kind}.defaultScope must be repository or organization.`); + } + if (policy.organizationVisibility !== undefined && !['all', 'private', 'selected'].includes(String(policy.organizationVisibility))) { + throw new Error(`storage.${kind}.organizationVisibility must be all, private, or selected.`); + } + if (policy.preserveExisting !== undefined && typeof policy.preserveExisting !== 'boolean') { + throw new Error(`storage.${kind}.preserveExisting must be a boolean.`); + } + if (policy.overrides !== undefined) { + validateObject(policy.overrides, `storage.${kind}.overrides`); + validateStringValues(policy.overrides as Record, `storage.${kind}.overrides`); + for (const [name, scope] of Object.entries(policy.overrides as Record)) { + if (!/^[A-Z][A-Z0-9_]*$/.test(name)) { + throw new Error(`storage.${kind}.overrides names must be uppercase GitHub Actions names.`); + } + if (!['repository', 'organization'].includes(String(scope))) { + throw new Error(`storage.${kind}.overrides.${name} must be repository or organization.`); + } + } + } + } +} + function validateSection( value: unknown, name: string, @@ -123,17 +161,20 @@ function validateBooleanProperty(value: Record, key: string): v if (value[key] !== undefined && typeof value[key] !== 'boolean') throw new Error(`${key} must be a boolean.`); } -function containsCredentialMaterial(value: unknown): boolean { +function containsCredentialMaterial(value: unknown, insideStorage = false): boolean { if (typeof value === 'string') { return /^(?:github_pat_|gh[pso]_|ghu_|ghs_|sk-|AIza|xox[baprs]-)/i.test(value.trim()); } if (!value || typeof value !== 'object') return false; - if (Array.isArray(value)) return value.some(containsCredentialMaterial); + if (Array.isArray(value)) return value.some(item => containsCredentialMaterial(item, insideStorage)); return Object.entries(value).some(([key, item]) => { + if (insideStorage) return false; + if (key === 'storage') return containsCredentialMaterial(item, true); // Boolean configuration switches such as `manageRepositorySecrets` and // `features.credentialHealth` are not credential material. Only reject // credential-shaped properties when they actually carry a value. - const looksLikeCredentialProperty = /(?:password|secret|token|api[_-]?key|credential)/i.test(key); + const looksLikeCredentialProperty = /(?:password|secret|token|api[_-]?key|credential)/i.test(key) + && !['storage', 'secrets', 'variables'].includes(key.toLowerCase()); return (looksLikeCredentialProperty && item !== undefined && item !== null && typeof item !== 'boolean') || containsCredentialMaterial(item); }); diff --git a/src/cli/setup_prompt_adapter.ts b/src/cli/setup_prompt_adapter.ts index 1348f91d..530aeea6 100644 --- a/src/cli/setup_prompt_adapter.ts +++ b/src/cli/setup_prompt_adapter.ts @@ -2,6 +2,7 @@ import { createInterface, type Interface } from 'node:readline/promises'; import { stdin, stdout } from 'node:process'; import type { SetupCredentialPromptPort, + SetupStoragePromptPort, SetupPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort, @@ -19,6 +20,10 @@ import type { SetupCredentialDecision, SetupCredentialValue, SetupWorkflowComparison, + SetupResourceStoragePolicy, + SetupStorageConfiguration, + SetupRemoteConfiguration, + SetupVariable, } from '../domain/setup'; const AGENT_PROVIDERS = ['codex', 'opencode', 'cursor'] as const; @@ -30,7 +35,7 @@ export interface SetupPromptAdapterOptions { credentialValues?: Record; } -export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { +export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromptPort, SetupStoragePromptPort, SetupWorkflowUpdatePromptPort, DoctorOutputPort { private readonly interactive: boolean; private readonly assumeYes: boolean; private readonly readline: Interface | undefined; @@ -130,6 +135,28 @@ export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromp return defaults; } + async chooseStorage( + defaults: SetupStorageConfiguration, + remote: SetupRemoteConfiguration, + variables: readonly SetupVariable[], + requirements: readonly SetupCredentialRequirement[], + managed: { secrets: boolean; variables: boolean } = { secrets: true, variables: true }, + ): Promise { + if (!this.readline) return defaults; + console.log(color('\n5. Review GitHub Actions resource scopes\n', 36)); + console.log(renderBox(renderRemoteConfiguration(remote, variables, requirements), 'Existing GitHub Actions resources', 33)); + + const secrets = managed.secrets + ? await this.chooseStoragePolicy('secrets', defaults.secrets, remote, requirements.map(requirement => requirement.name)) + : defaults.secrets; + const configuredVariables = variables.map(variable => variable.name); + const variableNames = configuredVariables.length > 0 ? configuredVariables : []; + const variablesPolicy = managed.variables + ? await this.chooseStoragePolicy('variables', defaults.variables, remote, variableNames) + : defaults.variables; + return { secrets, variables: variablesPolicy }; + } + showPlan(plan: SetupPlan): void { const enabledFeatures = Object.entries(plan.configuration.features) .filter(([, enabled]) => enabled) @@ -145,6 +172,8 @@ export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromp ` Files selected: ${plan.selectedFiles.length}`, ` Variables to upsert: ${plan.configuration.manageRepositoryVariables ? plan.variables.length : 0}`, ` Secrets to validate/provision: ${plan.configuration.manageRepositorySecrets ? plan.credentialRequirements.length : 0}`, + ` Variable storage: ${plan.configuration.storage.variables.defaultScope} scope${plan.configuration.storage.variables.defaultScope === 'organization' ? ` (${plan.configuration.storage.variables.organizationVisibility})` : ''}`, + ` Secret storage: ${plan.configuration.storage.secrets.defaultScope} scope${plan.configuration.storage.secrets.defaultScope === 'organization' ? ` (${plan.configuration.storage.secrets.organizationVisibility})` : ''}`, ` Labels and issue types: always checked by Copilot setup`, ` Initial tag: ${plan.configuration.createInitialTag ? 'v1.0.0 when no version tag exists' : 'disabled'}`, '', color('Credential contract', 33), ` ${plan.requiredSecrets.join(', ')}`, @@ -314,6 +343,51 @@ export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromp console.log(color('Please select one of the listed options.', 33)); } } + + private async chooseStoragePolicy( + kind: 'secrets' | 'variables', + defaults: SetupResourceStoragePolicy, + remote: SetupRemoteConfiguration, + names: readonly string[], + ): Promise { + const label = kind === 'secrets' ? 'Secrets' : 'Variables'; + const defaultScope = await this.askChoice( + `Where should new GitHub Actions ${label} be stored?`, + ['repository', 'organization'], + defaults.defaultScope, + ) as SetupResourceStoragePolicy['defaultScope']; + const organizationVisibility = (defaultScope === 'organization' || Object.values(defaults.overrides).includes('organization')) + ? await this.askChoice( + `How should organization ${label} be shared?`, + ['selected', 'private', 'all'], + defaults.organizationVisibility, + ) as SetupResourceStoragePolicy['organizationVisibility'] + : defaults.organizationVisibility; + const preserveExisting = await this.askBoolean( + `Preserve existing effective ${label} instead of creating a shadowing override?`, + defaults.preserveExisting, + ); + const organizationNames = kind === 'secrets' + ? remote.organizationSecrets + : remote.organizationVariables.map(variable => variable.name); + const repositoryNames = kind === 'secrets' + ? remote.repositorySecrets + : remote.repositoryVariables.map(variable => variable.name); + const inherited = names.filter(name => organizationNames.includes(name) && !repositoryNames.includes(name)); + let overrides = { ...defaults.overrides }; + if (inherited.length > 0 && defaultScope === 'repository') { + const overrideInput = await this.askText( + `Organization ${label} available to this repository: ${inherited.join(', ')}. Repository override names (comma-separated, empty to inherit all)`, + '', + ); + const requested = new Set(overrideInput.split(',').map(name => name.trim()).filter(Boolean)); + overrides = { + ...overrides, + ...Object.fromEntries(inherited.filter(name => requested.has(name)).map(name => [name, 'repository'])), + } as Record; + } + return { defaultScope, organizationVisibility, preserveExisting, overrides }; + } } function statusIcon(status: SetupCredentialCheck['status']): string { @@ -349,6 +423,27 @@ function renderBox(content: string, title: string, borderCode = 36): string { ].join('\n'); } +function renderRemoteConfiguration( + remote: SetupRemoteConfiguration, + variables: readonly SetupVariable[], + requirements: readonly SetupCredentialRequirement[], +): string { + const lines = [ + `Target owner: ${remote.ownerType}; repository visibility: ${remote.repositoryVisibility}; repository ID: ${remote.repositoryId ?? 'unknown'}`, + `Repository Secrets: ${remote.repositorySecrets.length > 0 ? remote.repositorySecrets.join(', ') : '(none detected)'}`, + `Organization Secrets available here: ${remote.organizationSecrets.length > 0 ? remote.organizationSecrets.join(', ') : '(none detected)'}`, + `Repository Variables: ${remote.repositoryVariables.length > 0 ? remote.repositoryVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, + `Organization Variables available here: ${remote.organizationVariables.length > 0 ? remote.organizationVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, + `Required Secrets: ${requirements.map(requirement => requirement.name).join(', ')}`, + `Required Variables: ${variables.map(variable => variable.name).join(', ')}`, + remote.organizationAccess === 'available' + ? 'Organization resources can be inspected for this repository.' + : `Organization resource inspection: ${remote.organizationAccess}.`, + 'Repository-level resources take precedence over organization-level resources. Secret values are never displayed.', + ]; + return lines.join('\n'); +} + function stripAnsi(value: string): string { return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''); } diff --git a/src/data/repository/__tests__/repository_variables_repository.test.ts b/src/data/repository/__tests__/repository_variables_repository.test.ts index 5412e446..f073fac9 100644 --- a/src/data/repository/__tests__/repository_variables_repository.test.ts +++ b/src/data/repository/__tests__/repository_variables_repository.test.ts @@ -60,4 +60,126 @@ describe('RepositoryVariablesRepository', () => { expect(payload).toMatchObject({ owner: 'owner', repo: 'repo', secret_name: 'PAT', key_id: 'key-id' }); expect(payload.encrypted_value).not.toContain('workflow-token'); }); + + it('inspects repository and organization resources without exposing secret values', async () => { + const client = { + rest: { + repos: { get: jest.fn().mockResolvedValue({ data: { id: 42, visibility: 'private', owner: { type: 'Organization' } } }) }, + actions: { + listRepoVariables: jest.fn().mockResolvedValue({ data: { variables: [{ name: 'REPO_VAR', value: 'repo' }] } }), + createRepoVariable: jest.fn(), updateRepoVariable: jest.fn(), + listRepoOrganizationVariables: jest.fn().mockResolvedValue({ data: { variables: [{ name: 'ORG_VAR', value: 'org' }] } }), + }, + secrets: { + listRepoSecrets: jest.fn().mockResolvedValue({ data: { secrets: [{ name: 'REPO_SECRET' }] } }), + listRepoOrganizationSecrets: jest.fn().mockResolvedValue({ data: { secrets: [{ name: 'ORG_SECRET' }] } }), + getRepoPublicKey: jest.fn(), createOrUpdateRepoSecret: jest.fn(), + }, + }, + }; + const repository = new RepositoryVariablesRepository({ getClient: jest.fn(() => client) }); + + await expect(repository.inspect('owner', 'repo', 'token')).resolves.toEqual(expect.objectContaining({ + ownerType: 'Organization', repositoryId: 42, repositoryVisibility: 'private', + repositorySecrets: ['REPO_SECRET'], organizationSecrets: ['ORG_SECRET'], + repositoryVariables: [{ name: 'REPO_VAR', value: 'repo' }], + organizationVariables: [{ name: 'ORG_VAR', value: 'org' }], + organizationSecretsAccess: 'available', organizationVariablesAccess: 'available', + })); + }); + + it('upserts selected organization secrets and variables with the repository access grant', async () => { + const createOrUpdateOrgSecret = jest.fn().mockResolvedValue(undefined); + const addSelectedRepoToOrgSecret = jest.fn().mockResolvedValue(undefined); + const createOrUpdateOrgVariable = jest.fn().mockResolvedValue(undefined); + const addSelectedRepoToOrgVariable = jest.fn().mockResolvedValue(undefined); + const client = { + rest: { + actions: { + listRepoVariables: jest.fn().mockResolvedValue({ data: { variables: [] } }), + createRepoVariable: jest.fn(), updateRepoVariable: jest.fn(), + listOrgVariables: jest.fn().mockResolvedValue({ data: { variables: [] } }), + createOrUpdateOrgVariable, addSelectedRepoToOrgVariable, + }, + secrets: { + listRepoSecrets: jest.fn().mockResolvedValue({ data: { secrets: [] } }), + getRepoPublicKey: jest.fn(), createOrUpdateRepoSecret: jest.fn(), + listOrgSecrets: jest.fn().mockResolvedValue({ data: { secrets: [] } }), + getOrgPublicKey: jest.fn().mockResolvedValue({ data: { key_id: 'org-key', key: randomBytes(32).toString('base64') } }), + createOrUpdateOrgSecret, addSelectedRepoToOrgSecret, + }, + }, + }; + const repository = new RepositoryVariablesRepository({ getClient: jest.fn(() => client) }); + const target = { scope: 'organization' as const, organizationVisibility: 'selected' as const, repositoryId: 42 }; + + await expect(repository.upsertScopedSecrets('owner', 'repo', 'token', target, [{ name: 'PAT', value: 'secret' }])) + .resolves.toMatchObject({ created: 1, errors: [] }); + await expect(repository.upsertScopedVariables('owner', 'repo', 'token', target, [{ name: 'MODE', value: 'strict' }])) + .resolves.toEqual({ created: 1, updated: 0, errors: [] }); + expect(createOrUpdateOrgSecret).toHaveBeenCalledWith(expect.objectContaining({ org: 'owner', visibility: 'selected', selected_repository_ids: [42] })); + expect(addSelectedRepoToOrgSecret).toHaveBeenCalledWith({ org: 'owner', secret_name: 'PAT', repository_id: 42 }); + expect(createOrUpdateOrgVariable).toHaveBeenCalledWith(expect.objectContaining({ org: 'owner', visibility: 'selected', selected_repository_ids: [42] })); + expect(addSelectedRepoToOrgVariable).toHaveBeenCalledWith({ org: 'owner', name: 'MODE', repository_id: 42 }); + }); + + it('reports unavailable organization inspection separately from personal repositories', async () => { + const personalClient = { + rest: { + repos: { get: jest.fn().mockResolvedValue({ data: { id: 1, visibility: 'public', owner: { type: 'User' } } }) }, + actions: { listRepoVariables: jest.fn().mockResolvedValue({ data: { variables: [] } }), createRepoVariable: jest.fn(), updateRepoVariable: jest.fn() }, + secrets: { listRepoSecrets: jest.fn().mockResolvedValue({ data: { secrets: [] } }), getRepoPublicKey: jest.fn(), createOrUpdateRepoSecret: jest.fn() }, + }, + }; + const personal = new RepositoryVariablesRepository({ getClient: jest.fn(() => personalClient) }); + await expect(personal.inspect('owner', 'repo', 'token')).resolves.toEqual(expect.objectContaining({ + ownerType: 'User', organizationAccess: 'not_applicable', + organizationSecretsAccess: 'not_applicable', organizationVariablesAccess: 'not_applicable', + })); + + const deniedClient = { + rest: { + repos: { get: jest.fn().mockResolvedValue({ data: { id: 1, visibility: 'internal', owner: { type: 'Organization' } } }) }, + actions: { + listRepoVariables: jest.fn().mockResolvedValue({ data: { variables: [] } }), createRepoVariable: jest.fn(), updateRepoVariable: jest.fn(), + listRepoOrganizationVariables: jest.fn().mockRejectedValue(new Error('variables forbidden')), + }, + secrets: { + listRepoSecrets: jest.fn().mockResolvedValue({ data: { secrets: [] } }), getRepoPublicKey: jest.fn(), createOrUpdateRepoSecret: jest.fn(), + listRepoOrganizationSecrets: jest.fn().mockRejectedValue(new Error('secrets forbidden')), + }, + }, + }; + const denied = new RepositoryVariablesRepository({ getClient: jest.fn(() => deniedClient) }); + await expect(denied.inspect('owner', 'repo', 'token')).resolves.toEqual(expect.objectContaining({ + ownerType: 'Organization', organizationAccess: 'unavailable', + organizationSecretsAccess: 'unavailable', organizationVariablesAccess: 'unavailable', + })); + }); + + it('uses the paginated client path and preserves existing organization visibility', async () => { + const paginate = jest.fn().mockResolvedValue([{ name: 'EXISTING', visibility: 'selected' }]); + const createOrUpdateOrgSecret = jest.fn().mockResolvedValue(undefined); + const client = { + paginate, + rest: { + actions: { + listRepoVariables: jest.fn(), createRepoVariable: jest.fn(), updateRepoVariable: jest.fn(), + listOrgVariables: jest.fn().mockResolvedValue({ data: { variables: [{ name: 'EXISTING_VAR', visibility: 'selected' }] } }), + createOrUpdateOrgVariable: jest.fn().mockResolvedValue(undefined), + }, + secrets: { + listRepoSecrets: jest.fn(), getRepoPublicKey: jest.fn(), createOrUpdateRepoSecret: jest.fn(), + listOrgSecrets: jest.fn(), getOrgPublicKey: jest.fn().mockResolvedValue({ data: { key_id: 'key', key: randomBytes(32).toString('base64') } }), + createOrUpdateOrgSecret, + }, + }, + }; + const repository = new RepositoryVariablesRepository({ getClient: jest.fn(() => client) }); + const target = { scope: 'organization' as const, organizationVisibility: 'all' as const, repositoryId: 9 }; + + await repository.upsertScopedSecrets('owner', 'repo', 'token', target, [{ name: 'EXISTING', value: 'new' }]); + expect(paginate).toHaveBeenCalled(); + expect(createOrUpdateOrgSecret).toHaveBeenCalledWith(expect.objectContaining({ visibility: 'selected' })); + }); }); diff --git a/src/data/repository/repository_variables_repository.ts b/src/data/repository/repository_variables_repository.ts index e769fbff..940ce25d 100644 --- a/src/data/repository/repository_variables_repository.ts +++ b/src/data/repository/repository_variables_repository.ts @@ -1,24 +1,63 @@ -import type { SetupRepositoryConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../application/ports/setup_wizard_ports'; -import type { SetupCredentialValue } from '../../domain/setup'; +import type { + SetupRemoteConfigurationReadPort, + SetupRepositoryConfigurationReadPort, + SetupRepositorySecretsPort, + SetupRepositoryVariablesPort, +} from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialValue, SetupRemoteConfiguration, SetupResourceTarget, SetupVariable } from '../../domain/setup'; import type { GithubClientPort } from '../../infrastructure/github/ports/github_client_provider_port'; -import type { GithubRepositoryVariablesClient } from '../../infrastructure/github/ports/github_repository_variables_protocol'; +import type { + GithubOrganizationResource, + GithubRepositoryVariablesClient, +} from '../../infrastructure/github/ports/github_repository_variables_protocol'; import nacl from 'tweetnacl'; import { createHash } from 'node:crypto'; -export class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort { +export class RepositoryVariablesRepository implements SetupRepositoryVariablesPort, SetupRepositorySecretsPort, SetupRepositoryConfigurationReadPort, SetupRemoteConfigurationReadPort { constructor(private readonly githubClient: GithubClientPort) {} async list(owner: string, repository: string, token: string): Promise { const client = this.githubClient.getClient(token); if (!client.rest.secrets) throw new Error('GitHub repository Secret API is unavailable.'); - const response = await client.rest.secrets.listRepoSecrets({ owner, repo: repository, per_page: 100 }); - return response.data.secrets.map(secret => secret.name); + const secrets = await listCollection(client, client.rest.secrets.listRepoSecrets, { owner, repo: repository, per_page: 100 }, 'secrets'); + return secrets.map(secret => secret.name); } async listVariables(owner: string, repository: string, token: string): Promise { const client = this.githubClient.getClient(token); - const response = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); - return response.data.variables.map(variable => ({ name: variable.name, value: variable.value })); + const variables = await listCollection(client, client.rest.actions.listRepoVariables, { owner, repo: repository, per_page: 100 }, 'variables'); + return variables.map(variable => ({ name: variable.name, ...(variable.value !== undefined ? { value: variable.value } : {}) })); + } + + async inspect(owner: string, repository: string, token: string): Promise { + const client = this.githubClient.getClient(token); + if (!client.rest.repos?.get) throw new Error('GitHub repository metadata API is unavailable.'); + const repositoryResponse = await client.rest.repos.get({ owner, repo: repository }); + const metadata = repositoryResponse.data; + const ownerType = normalizeOwnerType(metadata.owner?.type); + const repositoryVisibility = normalizeRepositoryVisibility(metadata.visibility); + const repositorySecrets = client.rest.secrets + ? await this.list(owner, repository, token) + : []; + const repositoryVariables = (await this.listVariables(owner, repository, token)) + .filter((variable): variable is SetupVariable => variable.value !== undefined) + .map(variable => ({ name: variable.name, value: variable.value })); + const organizationSecretsResult = await this.listOrganizationSecrets(client, owner, repository, ownerType); + const organizationVariablesResult = await this.listOrganizationVariables(client, owner, repository, ownerType); + return { + ownerType, + repositoryId: metadata.id, + repositoryVisibility, + repositorySecrets, + organizationSecrets: organizationSecretsResult.resources.map(resource => resource.name), + repositoryVariables, + organizationVariables: organizationVariablesResult.resources + .filter((resource): resource is GithubOrganizationResource & { value: string } => resource.value !== undefined) + .map(resource => ({ name: resource.name, value: resource.value })), + organizationAccess: combineOrganizationAccess(organizationSecretsResult.access, organizationVariablesResult.access), + organizationSecretsAccess: organizationSecretsResult.access, + organizationVariablesAccess: organizationVariablesResult.access, + }; } async upsertSecrets( @@ -53,6 +92,54 @@ export class RepositoryVariablesRepository implements SetupRepositoryVariablesPo return { created, updated, skipped, errors }; } + async upsertScopedSecrets( + owner: string, + repository: string, + token: string, + target: SetupResourceTarget, + credentials: readonly SetupCredentialValue[], + ): Promise<{ created: number; updated: number; skipped: number; errors: string[] }> { + if (target.scope === 'repository') return this.upsertSecrets(owner, repository, token, credentials); + const client = this.githubClient.getClient(token); + const secrets = client.rest.secrets; + if (!secrets?.getOrgPublicKey || !secrets.createOrUpdateOrgSecret || !secrets.listOrgSecrets) { + throw new Error('GitHub organization Secret API is unavailable or the setup PAT lacks organization Secret permissions.'); + } + if (target.organizationVisibility === 'selected' && target.repositoryId === undefined) { + throw new Error('The repository ID is required for selected organization Secret access.'); + } + const existing = new Map((await listCollection(client, secrets.listOrgSecrets, { org: owner, per_page: 30 }, 'secrets')) + .map(secret => [secret.name, secret])); + const publicKey = await secrets.getOrgPublicKey({ org: owner }); + let created = 0; + let updated = 0; + const errors: string[] = []; + for (const credential of credentials) { + try { + const current = existing.get(credential.name); + const visibility = current?.visibility ?? target.organizationVisibility; + await secrets.createOrUpdateOrgSecret({ + org: owner, + secret_name: credential.name, + encrypted_value: encryptSecret(credential.value, publicKey.data.key), + key_id: publicKey.data.key_id, + visibility, + ...(visibility === 'selected' && target.repositoryId !== undefined && !current + ? { selected_repository_ids: [target.repositoryId] } + : {}), + }); + if (visibility === 'selected' && target.repositoryId !== undefined && secrets.addSelectedRepoToOrgSecret) { + await secrets.addSelectedRepoToOrgSecret({ org: owner, secret_name: credential.name, repository_id: target.repositoryId }); + } + if (current) updated += 1; + else created += 1; + } catch (error) { + errors.push(`Error configuring organization Secret ${credential.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, skipped: 0, errors }; + } + /** Alias kept separate from Variables so callers cannot accidentally mix the two operations. */ async upsert( owner: string, @@ -70,8 +157,8 @@ export class RepositoryVariablesRepository implements SetupRepositoryVariablesPo variables: readonly { name: string; value: string }[], ): Promise<{ created: number; updated: number; errors: string[] }> { const client = this.githubClient.getClient(token); - const existing = await client.rest.actions.listRepoVariables({ owner, repo: repository, per_page: 100 }); - const existingValues = new Map(existing.data.variables.map(variable => [variable.name, variable.value])); + const existingVariables = await listCollection(client, client.rest.actions.listRepoVariables, { owner, repo: repository, per_page: 100 }, 'variables'); + const existingValues = new Map(existingVariables.map(variable => [variable.name, variable.value])); let created = 0; let updated = 0; const errors: string[] = []; @@ -92,6 +179,113 @@ export class RepositoryVariablesRepository implements SetupRepositoryVariablesPo } return { created, updated, errors }; } + + async upsertScopedVariables( + owner: string, + repository: string, + token: string, + target: SetupResourceTarget, + variables: readonly SetupVariable[], + ): Promise<{ created: number; updated: number; errors: string[] }> { + if (target.scope === 'repository') return this.upsert(owner, repository, token, variables); + const client = this.githubClient.getClient(token); + const actions = client.rest.actions; + if (!actions.listOrgVariables || !actions.createOrUpdateOrgVariable) { + throw new Error('GitHub organization Variable API is unavailable or the setup PAT lacks organization Variable permissions.'); + } + if (target.organizationVisibility === 'selected' && target.repositoryId === undefined) { + throw new Error('The repository ID is required for selected organization Variable access.'); + } + const existing = new Map((await listCollection(client, actions.listOrgVariables, { org: owner, per_page: 30 }, 'variables')) + .map(variable => [variable.name, variable])); + let created = 0; + let updated = 0; + const errors: string[] = []; + for (const variable of variables) { + try { + const current = existing.get(variable.name); + const visibility = current?.visibility ?? target.organizationVisibility; + await actions.createOrUpdateOrgVariable({ + org: owner, + name: variable.name, + value: variable.value, + visibility, + ...(visibility === 'selected' && target.repositoryId !== undefined && !current + ? { selected_repository_ids: [target.repositoryId] } + : {}), + }); + if (visibility === 'selected' && target.repositoryId !== undefined && actions.addSelectedRepoToOrgVariable) { + await actions.addSelectedRepoToOrgVariable({ org: owner, name: variable.name, repository_id: target.repositoryId }); + } + if (current) updated += 1; + else created += 1; + } catch (error) { + errors.push(`Error configuring organization Variable ${variable.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { created, updated, errors }; + } + + private async listOrganizationSecrets( + client: GithubRepositoryVariablesClient, + owner: string, + repository: string, + ownerType: SetupRemoteConfiguration['ownerType'], + ): Promise<{ resources: GithubOrganizationResource[]; access: SetupRemoteConfiguration['organizationSecretsAccess'] }> { + if (ownerType !== 'Organization') return { resources: [], access: 'not_applicable' }; + const list = client.rest.secrets?.listRepoOrganizationSecrets; + if (!list) return { resources: [], access: 'unknown' }; + try { + return { resources: await listCollection(client, list, { owner, repo: repository, per_page: 30 }, 'secrets'), access: 'available' }; + } catch { + return { resources: [], access: 'unavailable' }; + } + } + + private async listOrganizationVariables( + client: GithubRepositoryVariablesClient, + owner: string, + repository: string, + ownerType: SetupRemoteConfiguration['ownerType'], + ): Promise<{ resources: GithubOrganizationResource[]; access: SetupRemoteConfiguration['organizationVariablesAccess'] }> { + if (ownerType !== 'Organization') return { resources: [], access: 'not_applicable' }; + const list = client.rest.actions.listRepoOrganizationVariables; + if (!list) return { resources: [], access: 'unknown' }; + try { + return { resources: await listCollection(client, list, { owner, repo: repository, per_page: 30 }, 'variables'), access: 'available' }; + } catch { + return { resources: [], access: 'unavailable' }; + } + } +} + +async function listCollection( + client: GithubRepositoryVariablesClient, + method: (parameters: Record) => Promise<{ data: T[] | { variables?: T[]; secrets?: T[] } }>, + parameters: Record, + key: 'variables' | 'secrets', +): Promise { + if (client.paginate) return client.paginate(method, parameters); + const response = await method(parameters); + return Array.isArray(response.data) ? response.data : response.data[key] ?? []; +} + +function normalizeOwnerType(value: string | undefined): SetupRemoteConfiguration['ownerType'] { + return value === 'Organization' ? 'Organization' : value === 'User' ? 'User' : 'Unknown'; +} + +function normalizeRepositoryVisibility(value: string | undefined): SetupRemoteConfiguration['repositoryVisibility'] { + return value === 'public' || value === 'private' || value === 'internal' ? value : 'unknown'; +} + +function combineOrganizationAccess( + secrets: SetupRemoteConfiguration['organizationSecretsAccess'], + variables: SetupRemoteConfiguration['organizationVariablesAccess'], +): SetupRemoteConfiguration['organizationAccess'] { + if (secrets === 'not_applicable' && variables === 'not_applicable') return 'not_applicable'; + if (secrets === 'available' || variables === 'available') return 'available'; + if (secrets === 'unavailable' || variables === 'unavailable') return 'unavailable'; + return 'unknown'; } /** GitHub requires a sealed box: ephemeral public key + crypto_box ciphertext. */ diff --git a/src/domain/setup.ts b/src/domain/setup.ts index 3b3eaf1b..f4ceea15 100644 --- a/src/domain/setup.ts +++ b/src/domain/setup.ts @@ -79,6 +79,31 @@ export interface SetupConfiguration { manageRepositorySecrets: boolean; /** Extra non-secret action inputs accepted by config files for advanced use cases. */ actionInputs: Record; + /** Independent storage policies for non-sensitive variables and secrets. */ + storage: SetupStorageConfiguration; +} + +export type SetupResourceScope = 'repository' | 'organization'; +export type SetupOrganizationVisibility = 'all' | 'private' | 'selected'; + +export interface SetupResourceStoragePolicy { + defaultScope: SetupResourceScope; + organizationVisibility: SetupOrganizationVisibility; + /** Keep an already-effective resource instead of creating a shadowing override. */ + preserveExisting: boolean; + /** Per-resource exceptions for mixed repository/organization configurations. */ + overrides: Record; +} + +export interface SetupResourceTarget { + scope: SetupResourceScope; + organizationVisibility: SetupOrganizationVisibility; + repositoryId?: number; +} + +export interface SetupStorageConfiguration { + secrets: SetupResourceStoragePolicy; + variables: SetupResourceStoragePolicy; } export type SetupCredentialKind = 'workflowPat' | 'apiKey'; @@ -98,6 +123,7 @@ export interface SetupCredentialCheck { status: SetupCredentialStatus; message: string; account?: string; + sourceScope?: SetupResourceScope; } export interface SetupCredentialValue { @@ -112,6 +138,22 @@ export interface SetupCredentialCollection { apiKeys: SetupCredentialValue[]; } +export type SetupOwnerType = 'User' | 'Organization' | 'Unknown'; +export type SetupRepositoryVisibility = 'public' | 'private' | 'internal' | 'unknown'; + +export interface SetupRemoteConfiguration { + ownerType: SetupOwnerType; + repositoryId?: number; + repositoryVisibility: SetupRepositoryVisibility; + repositorySecrets: readonly string[]; + organizationSecrets: readonly string[]; + repositoryVariables: readonly SetupVariable[]; + organizationVariables: readonly SetupVariable[]; + organizationAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; + organizationSecretsAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; + organizationVariablesAccess: 'available' | 'unavailable' | 'not_applicable' | 'unknown'; +} + export interface SetupWorkflowComparison { file: string; destination: string; diff --git a/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts b/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts index d59fa30e..ca387974 100644 --- a/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts @@ -50,7 +50,7 @@ describe('initial setup composition root', () => { ); expect(mockComposeInitialSetupUseCase).toHaveBeenCalledTimes(1); const dependencies = mockComposeInitialSetupUseCase.mock.calls[0]; - expect(dependencies).toHaveLength(9); + expect(dependencies).toHaveLength(10); expect(dependencies[1]).toBe(mockLabelProvisioning); }); }); diff --git a/src/infrastructure/composition/initial_setup_composition_root.ts b/src/infrastructure/composition/initial_setup_composition_root.ts index 1002b9b2..2c271bf8 100644 --- a/src/infrastructure/composition/initial_setup_composition_root.ts +++ b/src/infrastructure/composition/initial_setup_composition_root.ts @@ -31,5 +31,6 @@ export function createInitialSetupCompositionRoot(): InitialSetupUseCase { new SetupWorkspaceAdapter(), repositoryConfiguration, repositoryConfiguration, + repositoryConfiguration, ); } diff --git a/src/infrastructure/composition/setup_credentials_composition_root.ts b/src/infrastructure/composition/setup_credentials_composition_root.ts index 0312ba36..89b6df06 100644 --- a/src/infrastructure/composition/setup_credentials_composition_root.ts +++ b/src/infrastructure/composition/setup_credentials_composition_root.ts @@ -1,5 +1,5 @@ import { SetupCredentialsUseCase } from '../../application/usecases/setup/setup_credentials_use_case'; -import type { SetupCredentialPromptPort } from '../../application/ports/setup_wizard_ports'; +import type { SetupCredentialPromptPort, SetupRemoteConfigurationReadPort } from '../../application/ports/setup_wizard_ports'; import { SetupCredentialValidationAdapter } from '../setup_credential_validation_adapter'; import { RepositoryVariablesRepository } from '../../data/repository/repository_variables_repository'; import { createRepositoryVariablesClient } from './github_identity_client_factory'; @@ -15,3 +15,7 @@ export function createSetupCredentialsUseCase(prompt: SetupCredentialPromptPort) new SetupRemoteCredentialHealthAdapter(new OctokitCredentialHealthClientAdapter(), { bootstrapWhenMissing: true }), ); } + +export function createSetupRemoteConfigurationReadPort(): SetupRemoteConfigurationReadPort { + return new RepositoryVariablesRepository(createRepositoryVariablesClient()); +} diff --git a/src/infrastructure/composition/setup_doctor_composition_root.ts b/src/infrastructure/composition/setup_doctor_composition_root.ts index 2248445b..fe2c19a7 100644 --- a/src/infrastructure/composition/setup_doctor_composition_root.ts +++ b/src/infrastructure/composition/setup_doctor_composition_root.ts @@ -16,5 +16,6 @@ export function createSetupDoctorUseCase(output: DoctorOutputPort): SetupDoctorU new SetupWorkspaceAdapter(), output, new SetupRemoteCredentialHealthAdapter(new OctokitCredentialHealthClientAdapter()), + repositoryConfiguration, ); } diff --git a/src/infrastructure/github/ports/github_repository_variables_protocol.ts b/src/infrastructure/github/ports/github_repository_variables_protocol.ts index 677ba3f4..31fb4011 100644 --- a/src/infrastructure/github/ports/github_repository_variables_protocol.ts +++ b/src/infrastructure/github/ports/github_repository_variables_protocol.ts @@ -3,19 +3,53 @@ export interface GithubRepositoryVariable { value?: string; } +export interface GithubOrganizationResource { + name: string; + value?: string; + visibility?: 'all' | 'private' | 'selected'; + selected_repositories_url?: string; +} + +export interface GithubRepositoryMetadata { + id?: number; + visibility?: string; + owner?: { type?: string }; +} + +export interface GithubActionsPublicKey { + key_id: string; + key: string; +} + export interface GithubRepositoryVariablesClient { rest: { + repos?: { + get(parameters: Record): Promise<{ data: GithubRepositoryMetadata }>; + }; actions: { listRepoVariables(parameters: Record): Promise<{ data: { variables: GithubRepositoryVariable[] } }>; createRepoVariable(parameters: Record): Promise; updateRepoVariable(parameters: Record): Promise; + listRepoOrganizationVariables?: (parameters: Record) => Promise<{ data: { variables: GithubOrganizationResource[] } }>; + listOrgVariables?: (parameters: Record) => Promise<{ data: { variables: GithubOrganizationResource[] } }>; + createOrUpdateOrgVariable?: (parameters: Record) => Promise; + addSelectedRepoToOrgVariable?: (parameters: Record) => Promise; }; secrets?: { listRepoSecrets(parameters: Record): Promise<{ data: { secrets: GithubRepositorySecret[] } }>; - getRepoPublicKey(parameters: Record): Promise<{ data: { key_id: string; key: string } }>; + getRepoPublicKey(parameters: Record): Promise<{ data: GithubActionsPublicKey }>; createOrUpdateRepoSecret(parameters: Record): Promise; + listRepoOrganizationSecrets?: (parameters: Record) => Promise<{ data: { secrets: GithubOrganizationResource[] } }>; + listOrgSecrets?: (parameters: Record) => Promise<{ data: { secrets: GithubOrganizationResource[] } }>; + getOrgPublicKey?: (parameters: Record) => Promise<{ data: GithubActionsPublicKey }>; + createOrUpdateOrgSecret?: (parameters: Record) => Promise; + addSelectedRepoToOrgSecret?: (parameters: Record) => Promise; }; }; + paginate?: ( + method: (parameters: Record) => Promise<{ data: T[] | { variables?: T[]; secrets?: T[] } }>, + parameters: Record, + ) => Promise; } export interface GithubRepositorySecret { From fa73c6e35079478b32b435d3aa0421163b47eb5f Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 4 Sep 2026 21:05:29 +0200 Subject: [PATCH 09/11] develop: close inactive waiting issues --- .../copilot_close_inactive_issues.yml | 47 + CONTRIBUTING.md | 8 +- _agent/docs/architecture.md | 77 +- _agent/docs/code-conventions.md | 6 +- _agent/docs/project-context.md | 4 +- _agent/docs/usecase-flows.md | 4 +- action.yml | 3 + build/cli/index.js | 2743 ++++++++++------- .../cli/src/actions/default_image_config.d.ts | 30 + .../actions/image_configuration_builder.d.ts | 2 +- .../actions/local_action_configuration.d.ts | 1 + .../local_action_configuration_sections.d.ts | 1 + .../src/application/contracts/input_keys.d.ts | 187 ++ .../contracts/product_identity.d.ts | 1 + .../policies/bugbot_constants.d.ts | 6 + .../setup_configuration_defaults.d.ts | 22 + .../policies/setup_configuration_plan.d.ts | 6 + .../policies/setup_configuration_policy.d.ts | 46 +- .../setup_configuration_storage_policy.d.ts | 15 + .../setup_configuration_validation.d.ts | 2 + .../policies/workflow_queue_policy.d.ts | 2 +- .../ports/issue_inactivity_ports.d.ts | 8 + .../close_inactive_issues_use_case.d.ts | 14 + .../close_inactive_issues_workflow.d.ts | 11 + .../actions/initial_setup_request.d.ts | 14 + .../actions/initial_setup_workflow.d.ts | 11 +- .../actions/setup_resource_provisioning.d.ts | 33 + .../usecases/single_action_use_case.d.ts | 3 +- .../usecases/single_action_workflow.d.ts | 1 + build/cli/src/cli/cli_errors.d.ts | 3 + build/cli/src/cli/setup_prompt_rendering.d.ts | 7 + build/cli/src/data/model/action_types.d.ts | 1 + build/cli/src/data/model/execution.d.ts | 1 + .../src/data/model/execution_components.d.ts | 1 + build/cli/src/data/model/single_action.d.ts | 1 + .../data/repository/ai/agent_constants.d.ts | 2 + .../issue/issue_inactivity_repository.d.ts | 11 + .../repository/workflow/workflow_status.d.ts | 13 + build/cli/src/domain/issue_inactivity.d.ts | 30 + build/cli/src/domain/setup.d.ts | 3 +- .../github_issue_client_factory.d.ts | 3 +- .../issue_inactivity_composition_root.d.ts | 2 + .../github/octokit_issue_adapters.d.ts | 5 +- .../ports/github_issue_provider_ports.d.ts | 28 + ...system_issue_inactivity_clock_adapter.d.ts | 4 + build/github_action/index.js | 2575 +++++++++------- .../src/actions/default_image_config.d.ts | 30 + .../actions/image_configuration_builder.d.ts | 2 +- .../actions/local_action_configuration.d.ts | 1 + .../local_action_configuration_sections.d.ts | 1 + .../src/application/contracts/input_keys.d.ts | 187 ++ .../contracts/product_identity.d.ts | 1 + .../policies/bugbot_constants.d.ts | 6 + .../setup_configuration_defaults.d.ts | 22 + .../policies/setup_configuration_plan.d.ts | 6 + .../policies/setup_configuration_policy.d.ts | 46 +- .../setup_configuration_storage_policy.d.ts | 15 + .../setup_configuration_validation.d.ts | 2 + .../policies/workflow_queue_policy.d.ts | 2 +- .../ports/issue_inactivity_ports.d.ts | 8 + .../close_inactive_issues_use_case.d.ts | 14 + .../close_inactive_issues_workflow.d.ts | 11 + .../actions/initial_setup_request.d.ts | 14 + .../actions/initial_setup_workflow.d.ts | 11 +- .../actions/setup_resource_provisioning.d.ts | 33 + .../usecases/single_action_use_case.d.ts | 3 +- .../usecases/single_action_workflow.d.ts | 1 + build/github_action/src/cli/cli_errors.d.ts | 3 + .../src/cli/setup_prompt_rendering.d.ts | 7 + .../src/data/model/action_types.d.ts | 1 + .../src/data/model/execution.d.ts | 1 + .../src/data/model/execution_components.d.ts | 1 + .../src/data/model/single_action.d.ts | 1 + .../data/repository/ai/agent_constants.d.ts | 2 + .../issue/issue_inactivity_repository.d.ts | 11 + .../repository/workflow/workflow_status.d.ts | 13 + .../src/domain/issue_inactivity.d.ts | 30 + build/github_action/src/domain/setup.d.ts | 3 +- .../github_issue_client_factory.d.ts | 3 +- .../issue_inactivity_composition_root.d.ts | 2 + .../github/octokit_issue_adapters.d.ts | 5 +- .../ports/github_issue_provider_ports.d.ts | 28 + ...system_issue_inactivity_clock_adapter.d.ts | 4 + docs/configuration.mdx | 3 + docs/dependency-rules.md | 22 +- docs/development/architecture.mdx | 29 +- docs/development/testing.mdx | 2 +- docs/features.mdx | 18 +- docs/graphify-development.md | 18 +- docs/how-to-use.mdx | 8 + docs/issues/configuration.mdx | 3 +- docs/issues/index.mdx | 3 +- docs/issues/notifications-and-auto-close.mdx | 16 +- docs/issues/workflow-setup.mdx | 28 + docs/single-actions/available-actions.mdx | 3 + docs/single-actions/configuration.mdx | 5 + docs/single-actions/index.mdx | 5 +- docs/single-actions/workflow-and-cli.mdx | 4 +- scripts/validate-workflow-contract.cjs | 1 + .../copilot_close_inactive_issues.yml | 47 + src/__tests__/cli.test.ts | 3 +- .../__tests__/agent_input_builder.test.ts | 2 +- src/actions/__tests__/common_action.test.ts | 1 + src/actions/__tests__/github_action.test.ts | 3 +- .../image_configuration_builder.test.ts | 3 +- src/actions/__tests__/local_action.test.ts | 2 +- src/actions/agent_input_builder.ts | 2 +- .../default_image_config.ts} | 280 +- src/actions/github_action.ts | 2 +- src/actions/github_action_ai_inputs.ts | 3 +- src/actions/github_action_branch_inputs.ts | 2 +- src/actions/github_action_execution.ts | 14 +- .../github_action_issue_type_inputs.ts | 2 +- src/actions/github_action_label_inputs.ts | 2 +- src/actions/github_action_locale_inputs.ts | 2 +- src/actions/github_action_project_inputs.ts | 2 +- src/actions/github_action_threshold_inputs.ts | 2 +- src/actions/github_action_workflow_inputs.ts | 2 +- src/actions/image_configuration_builder.ts | 3 +- .../local_action_configuration_sections.ts | 9 +- src/actions/local_action_execution.ts | 2 + src/actions/local_action_output.ts | 2 +- src/actions/main_run_lifecycle.ts | 2 +- .../__tests__/architecture_boundaries.test.ts | 1 - src/application/contracts/input_keys.ts | 236 ++ src/application/contracts/product_identity.ts | 1 + .../setup_configuration_policy.test.ts | 19 + src/application/policies/bugbot_constants.ts | 8 + .../policies/result_publication_policy.ts | 2 +- .../policies/setup_configuration_defaults.ts | 172 ++ .../policies/setup_configuration_plan.ts | 234 ++ .../policies/setup_configuration_policy.ts | 588 +--- .../setup_configuration_storage_policy.ts | 158 + .../setup_configuration_validation.ts | 55 + .../policies/workflow_queue_policy.ts | 1 + .../ports/issue_inactivity_ports.ts | 20 + .../__tests__/single_action_use_case.test.ts | 30 +- .../close_inactive_issues_use_case.test.ts | 142 + .../__tests__/create_release_use_case.test.ts | 2 +- .../__tests__/create_tag_use_case.test.ts | 2 +- .../__tests__/initial_setup_request.test.ts | 53 + .../publish_github_action_use_case.test.ts | 2 +- .../setup_resource_provisioning.test.ts | 159 + .../actions/close_inactive_issues_use_case.ts | 25 + .../actions/close_inactive_issues_workflow.ts | 164 + .../usecases/actions/create_release_policy.ts | 2 +- .../usecases/actions/create_tag_workflow.ts | 2 +- .../usecases/actions/initial_setup_request.ts | 40 + .../actions/initial_setup_use_case.ts | 3 +- .../actions/initial_setup_workflow.ts | 254 +- .../actions/publish_github_action_workflow.ts | 2 +- .../actions/setup_resource_provisioning.ts | 181 ++ .../execution_issue_number_policy.ts | 2 +- .../usecases/single_action_use_case.ts | 2 + .../usecases/single_action_workflow.ts | 6 +- .../bugbot/__tests__/limit_comments.test.ts | 2 +- .../commit/bugbot/apply_detected_findings.ts | 2 +- .../steps/commit/bugbot/limit_comments.ts | 2 +- .../usecases/steps/commit/bugbot/marker.ts | 2 +- .../production_dependency_boundaries.test.ts | 79 + .../__tests__/setup_prompt_rendering.test.ts | 88 + src/cli/cli_errors.ts | 3 + .../detect_potential_problems_policy.test.ts | 3 +- .../__tests__/issue_command_policy.test.ts | 3 +- .../commands/__tests__/setup_policy.test.ts | 3 +- src/cli/commands/check_progress.ts | 2 +- src/cli/commands/detect_potential_problems.ts | 2 +- .../detect_potential_problems_policy.ts | 3 +- src/cli/commands/do.ts | 2 +- src/cli/commands/issue_command_policy.ts | 3 +- src/cli/commands/recommend_steps.ts | 2 +- src/cli/commands/setup.ts | 2 +- src/cli/commands/setup_policy.ts | 3 +- src/cli/commands/think.ts | 2 +- src/cli/commands/think_command_handler.ts | 3 +- src/cli/setup_config_file.ts | 2 +- src/cli/setup_prompt_adapter.ts | 67 +- src/cli/setup_prompt_rendering.ts | 66 + src/cli_context.ts | 2 +- src/data/model/__tests__/execution.test.ts | 3 +- .../__tests__/initial_labels_policy.test.ts | 2 +- .../model/__tests__/single_action.test.ts | 13 +- src/data/model/action_types.ts | 1 + src/data/model/execution.ts | 3 + src/data/model/execution_components.ts | 1 + src/data/model/single_action.ts | 7 + .../repository/ai/agent_capability_adapter.ts | 2 +- src/data/repository/ai/agent_constants.ts | 2 + .../issue_inactivity_repository.test.ts | 113 + .../issue/issue_inactivity_repository.ts | 67 + ..._previous_workflow_runs_repository.test.ts | 8 +- ...ctive_previous_workflow_runs_repository.ts | 6 +- .../repository/workflow/workflow_status.ts | 20 + src/domain/__tests__/issue_inactivity.test.ts | 63 + src/domain/issue_inactivity.ts | 82 + src/domain/setup.ts | 2 + .../github_issue_client_factory.ts | 3 +- .../issue_inactivity_composition_root.ts | 13 + .../main_run_route_composition_root.ts | 2 + .../github/octokit_issue_adapters.ts | 5 +- .../ports/github_issue_provider_ports.ts | 23 + ...tem_issue_inactivity_clock_adapter.test.ts | 11 + .../system_issue_inactivity_clock_adapter.ts | 7 + src/utils/setup_files.ts | 2 + 204 files changed, 6942 insertions(+), 3526 deletions(-) create mode 100644 .github/workflows/copilot_close_inactive_issues.yml create mode 100644 build/cli/src/actions/default_image_config.d.ts create mode 100644 build/cli/src/application/contracts/input_keys.d.ts create mode 100644 build/cli/src/application/contracts/product_identity.d.ts create mode 100644 build/cli/src/application/policies/bugbot_constants.d.ts create mode 100644 build/cli/src/application/policies/setup_configuration_defaults.d.ts create mode 100644 build/cli/src/application/policies/setup_configuration_plan.d.ts create mode 100644 build/cli/src/application/policies/setup_configuration_storage_policy.d.ts create mode 100644 build/cli/src/application/policies/setup_configuration_validation.d.ts create mode 100644 build/cli/src/application/ports/issue_inactivity_ports.d.ts create mode 100644 build/cli/src/application/usecases/actions/close_inactive_issues_use_case.d.ts create mode 100644 build/cli/src/application/usecases/actions/close_inactive_issues_workflow.d.ts create mode 100644 build/cli/src/application/usecases/actions/initial_setup_request.d.ts create mode 100644 build/cli/src/application/usecases/actions/setup_resource_provisioning.d.ts create mode 100644 build/cli/src/cli/cli_errors.d.ts create mode 100644 build/cli/src/cli/setup_prompt_rendering.d.ts create mode 100644 build/cli/src/data/repository/ai/agent_constants.d.ts create mode 100644 build/cli/src/data/repository/issue/issue_inactivity_repository.d.ts create mode 100644 build/cli/src/data/repository/workflow/workflow_status.d.ts create mode 100644 build/cli/src/domain/issue_inactivity.d.ts create mode 100644 build/cli/src/infrastructure/composition/issue_inactivity_composition_root.d.ts create mode 100644 build/cli/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts create mode 100644 build/github_action/src/actions/default_image_config.d.ts create mode 100644 build/github_action/src/application/contracts/input_keys.d.ts create mode 100644 build/github_action/src/application/contracts/product_identity.d.ts create mode 100644 build/github_action/src/application/policies/bugbot_constants.d.ts create mode 100644 build/github_action/src/application/policies/setup_configuration_defaults.d.ts create mode 100644 build/github_action/src/application/policies/setup_configuration_plan.d.ts create mode 100644 build/github_action/src/application/policies/setup_configuration_storage_policy.d.ts create mode 100644 build/github_action/src/application/policies/setup_configuration_validation.d.ts create mode 100644 build/github_action/src/application/ports/issue_inactivity_ports.d.ts create mode 100644 build/github_action/src/application/usecases/actions/close_inactive_issues_use_case.d.ts create mode 100644 build/github_action/src/application/usecases/actions/close_inactive_issues_workflow.d.ts create mode 100644 build/github_action/src/application/usecases/actions/initial_setup_request.d.ts create mode 100644 build/github_action/src/application/usecases/actions/setup_resource_provisioning.d.ts create mode 100644 build/github_action/src/cli/cli_errors.d.ts create mode 100644 build/github_action/src/cli/setup_prompt_rendering.d.ts create mode 100644 build/github_action/src/data/repository/ai/agent_constants.d.ts create mode 100644 build/github_action/src/data/repository/issue/issue_inactivity_repository.d.ts create mode 100644 build/github_action/src/data/repository/workflow/workflow_status.d.ts create mode 100644 build/github_action/src/domain/issue_inactivity.d.ts create mode 100644 build/github_action/src/infrastructure/composition/issue_inactivity_composition_root.d.ts create mode 100644 build/github_action/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts create mode 100644 setup/workflows/copilot_close_inactive_issues.yml rename src/{utils/constants.ts => actions/default_image_config.ts} (68%) create mode 100644 src/application/contracts/input_keys.ts create mode 100644 src/application/contracts/product_identity.ts create mode 100644 src/application/policies/bugbot_constants.ts create mode 100644 src/application/policies/setup_configuration_defaults.ts create mode 100644 src/application/policies/setup_configuration_plan.ts create mode 100644 src/application/policies/setup_configuration_storage_policy.ts create mode 100644 src/application/policies/setup_configuration_validation.ts create mode 100644 src/application/ports/issue_inactivity_ports.ts create mode 100644 src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts create mode 100644 src/application/usecases/actions/__tests__/initial_setup_request.test.ts create mode 100644 src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts create mode 100644 src/application/usecases/actions/close_inactive_issues_use_case.ts create mode 100644 src/application/usecases/actions/close_inactive_issues_workflow.ts create mode 100644 src/application/usecases/actions/initial_setup_request.ts create mode 100644 src/application/usecases/actions/setup_resource_provisioning.ts create mode 100644 src/architecture/__tests__/production_dependency_boundaries.test.ts create mode 100644 src/cli/__tests__/setup_prompt_rendering.test.ts create mode 100644 src/cli/cli_errors.ts create mode 100644 src/cli/setup_prompt_rendering.ts create mode 100644 src/data/repository/ai/agent_constants.ts create mode 100644 src/data/repository/issue/__tests__/issue_inactivity_repository.test.ts create mode 100644 src/data/repository/issue/issue_inactivity_repository.ts create mode 100644 src/data/repository/workflow/workflow_status.ts create mode 100644 src/domain/__tests__/issue_inactivity.test.ts create mode 100644 src/domain/issue_inactivity.ts create mode 100644 src/infrastructure/composition/issue_inactivity_composition_root.ts create mode 100644 src/infrastructure/time/__tests__/system_issue_inactivity_clock_adapter.test.ts create mode 100644 src/infrastructure/time/system_issue_inactivity_clock_adapter.ts diff --git a/.github/workflows/copilot_close_inactive_issues.yml b/.github/workflows/copilot_close_inactive_issues.yml new file mode 100644 index 00000000..514f90cf --- /dev/null +++ b/.github/workflows/copilot_close_inactive_issues.yml @@ -0,0 +1,47 @@ +name: Copilot - Close Inactive Issues + +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + inactivity_threshold_hours: + description: Hours without activity before closing a waiting issue + required: false + default: '168' + type: string + +permissions: + contents: read + +jobs: + copilot-inactive-issues: + name: Copilot - Close Inactive Issues + runs-on: [self-hosted, codex] + timeout-minutes: 120 + permissions: + contents: read + steps: + - name: Checkout Repository + uses: actions/checkout@v5 + + - uses: ./ + with: + single-action: close_inactive_issues_action + inactivity-threshold-hours: ${{ inputs.inactivity_threshold_hours || vars.INACTIVITY_THRESHOLD_HOURS || '168' }} + agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} + agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} + agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} + agent-effort: ${{ vars.AGENT_EFFORT }} + agent-command: ${{ vars.AGENT_COMMAND }} + findings-provider: ${{ vars.FINDINGS_PROVIDER }} + findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} + findings-model: ${{ vars.FINDINGS_MODEL }} + findings-effort: ${{ vars.FINDINGS_EFFORT }} + findings-command: ${{ vars.FINDINGS_COMMAND }} + fixer-provider: ${{ vars.FIXER_PROVIDER }} + fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} + fixer-model: ${{ vars.FIXER_MODEL }} + fixer-effort: ${{ vars.FIXER_EFFORT }} + fixer-command: ${{ vars.FIXER_COMMAND }} + token: ${{ secrets.PAT }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7cb3ffa..ca1c29ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,20 +39,20 @@ pnpm run build - **`src/infrastructure/composition/`** – Capability and use-case composition roots. - **`src/infrastructure/github/`** – GitHub provider clients and transport adapters. - **`src/manager/`** – Content handlers for PR descriptions, hotfix changelog, and markdown (e.g. `configuration_handler`, `markdown_content_hotfix_handler`). -- **`src/data/model/`** – Models and model policies; the directory still contains some transitional orchestration and is not uniformly pure domain. +- **`src/data/model/`** – Provider-neutral models and pure model policies. Runtime orchestration belongs under `src/application/usecases/`. - **`src/data/repository/`** – Specialized capability adapters and repository policies. -- **`src/utils/`** – Constants, logger, content utils, etc. +- **`src/utils/`** – Small side-effect-free utilities and the concrete logger; feature contracts live with their owning layer. - **`action.yml`** – Action metadata and input definitions. - **`build/`** – Compiled output (bundled JS); do not edit directly. ## Conventions 1. **TypeScript** – Prefer TypeScript; keep action and CLI buildable with `ncc`. -2. **Constants** – Use `INPUT_KEYS` and `ACTIONS` from `src/utils/constants.ts` instead of ad-hoc strings. +2. **Contracts and constants** – Use `INPUT_KEYS` from `src/application/contracts/input_keys.ts` and `ACTIONS` from `src/data/model/action_types.ts` instead of ad-hoc strings. 3. **Logging** – Use the semantic application logging port from `src/application/ports/logging_ports.ts` in application code. Concrete logging adapters and direct `src/utils/logger.ts` usage belong only at the outer infrastructure/entrypoint boundary. 4. **New inputs** – When adding inputs: - Update `action.yml` - - Add to `INPUT_KEYS` in `src/utils/constants.ts` + - Add to `INPUT_KEYS` in `src/application/contracts/input_keys.ts` - Read the input through the appropriate input builder/reader used by the action or CLI composition root. ## Code Quality diff --git a/_agent/docs/architecture.md b/_agent/docs/architecture.md index ee8f9888..0aada8ec 100644 --- a/_agent/docs/architecture.md +++ b/_agent/docs/architecture.md @@ -5,74 +5,65 @@ description: Current architecture, runtime boundaries, and key source paths. # Architecture and key paths -This is a compact contributor guide for the current checkout. The authoritative -architecture contract is [`../../docs/repository-architecture.md`](../../docs/repository-architecture.md), -with the capability inventory in -[`../../docs/capability-map.md`](../../docs/capability-map.md). +The authoritative architecture contract is [`../../docs/development/architecture.mdx`](../../docs/development/architecture.mdx), with executable rules in [`../../docs/dependency-rules.md`](../../docs/dependency-rules.md). ## Dependency direction ```text entrypoint - -> lifecycle/capability composition + -> lifecycle/composition root -> application use case/workflow - -> semantic port + -> semantic application port -> specialized adapter -> provider client/detail ``` Application code must not import concrete repositories, manager adapters, -infrastructure, entrypoints, Octokit, or provider DTOs. +infrastructure, entrypoints, Octokit, or provider DTOs. `src/data/model/` and +`src/domain/` are the pure core; they may depend only on other pure policies +and standard TypeScript types. ## Runtime entrypoints 1. **GitHub Action:** `src/actions/github_action.ts` maps GitHub inputs/events, builds `Execution`, and enters the shared action lifecycle. -2. **Local action:** `src/actions/local_action.ts` builds local configuration and - execution, invokes `mainRun`, and renders local results. -3. **CLI:** `src/cli.ts` is a small bootstrap for `src/cli/cli_program.ts` and - command modules under `src/cli/commands/`. -4. **Main routing:** `src/actions/common_action.ts` and - `src/actions/main_run_dispatcher.ts` select issue, pull-request, comment, - push, and single-action workflows. +2. **Local action:** `src/actions/local_action.ts` maps local/config inputs, + builds `Execution`, and renders local results. +3. **CLI:** `src/cli.ts` boots `src/cli/cli_program.ts` and command modules. +4. **Main routing:** `src/actions/common_action.ts` coordinates lifecycle + concerns; `src/actions/main_run_dispatcher.ts` only logs and delegates to + handlers supplied by `src/infrastructure/composition/`. -These lifecycles are intentionally independent. +These lifecycles remain independent and share only provider-neutral contracts. ## Key paths | Area | Path | Purpose | |---|---|---| -| Application use cases | `src/application/usecases/` | workflows and orchestration | +| Domain/model policies | `src/domain/`, `src/data/model/` | provider-neutral rules and models | +| Application use cases | `src/application/usecases/` | orchestration and workflows | +| Application policies | `src/application/policies/` | deterministic decisions and mapping policies | | Semantic ports | `src/application/ports/` | capability contracts | -| Domain/model policies | `src/data/model/` | models and pure policies | | Specialized adapters | `src/data/repository/` | provider-facing capability implementations | | GitHub transports | `src/infrastructure/github/` | Octokit/GraphQL adapters and client ports | -| Composition roots | `src/infrastructure/composition/` | capability/use-case graph assembly | -| Runtime composition | `src/actions/` | GitHub/local lifecycle and route-specific wiring | -| Description/configuration adapter | `src/manager/description/` | issue-description persistence details | -| CLI commands | `src/cli/commands/` | parsing and command-specific entry behavior | -| Architecture tests | `src/application/__tests__/`, `src/actions/__tests__/`, `src/infrastructure/**/__tests__/` | executable dependency rules | +| Composition roots | `src/infrastructure/composition/` | dependency graph assembly | +| Runtime composition | `src/actions/` | GitHub/local lifecycle boundaries | +| Description adapter | `src/manager/description/` | issue-description persistence details | +| CLI commands | `src/cli/commands/` | parsing and command entry behavior | +| Architecture tests | `src/architecture/__tests__/`, `src/application/**/__tests__/` | executable dependency rules | -## Current architecture notes +## Current design notes -- There is no current universal `RepositoryFactory`, `AiRepository`, - `IssueRepository`, `PullRequestRepository`, `ProjectBoardRepository`, or - `OrganizationRepository` production facade. -- Project board query, link, and command capabilities share only the query - contract required by composition. -- Organization membership, authenticated identity, and actor authorization are - separate ports/adapters. -- Pull-request changes, review, threads, and lifecycle remain separate. -- `ConfigurationHandler` is an outer adapter behind application configuration - ports. -- Concrete route wiring still exists in `main_run_dispatcher.ts`; Phase D audits - whether that is the correct lifecycle boundary before moving any code. -- A verified import SCC is the next production priority: `Execution` initiates - release/hotfix resolution through model helpers, those helpers construct - application use cases, and those use cases import `Execution`; - `ExecutionConfigurationPort` and `Execution` also import each other. Remove - this through application-owned orchestration and an executable boundary guard - before continuing release/tag adapter hardening. +- There is no universal repository, AI, or provider facade in production. +- `Execution` is the legacy-compatible runtime aggregate and remains a high- + connectivity hub; new use cases should accept the narrowest context contract + that their capability needs. +- Setup configuration is split into focused defaults, plan, validation, and + storage policies. Resource provisioning is isolated from setup orchestration. +- Input keys, Bugbot constants, workflow statuses, image defaults, and CLI + errors are owned by their consuming layer instead of a global constants file. +- Architecture tests verify cycle freedom, import resolution, pure-core + isolation, application outer-layer isolation, and composition boundaries. -Never infer current architecture from historical plans without checking the -source and the authoritative documents. +Always inspect current source and run the architecture tests; generated Graphify +topology and historical documents are navigation aids, not authority. diff --git a/_agent/docs/code-conventions.md b/_agent/docs/code-conventions.md index cc9973b3..c82dc45c 100644 --- a/_agent/docs/code-conventions.md +++ b/_agent/docs/code-conventions.md @@ -7,13 +7,13 @@ description: Copilot – coding conventions and where to change things ## Logging and constants -- Use **logger**: `logInfo`, `logError`, `logDebugInfo` from `src/utils/logger`. No ad-hoc `console.log`. -- Use **constants**: `INPUT_KEYS` and `ACTIONS` from `src/utils/constants.ts` for input names and action names. No hardcoded strings for these. +- Use the semantic application logger: `logInfo`, `logError`, `logDebugInfo` from `src/application/ports/logging_ports.ts` in application code. Runtime adapters may use `src/utils/logger.ts`; application code must not. No ad-hoc `console.log`. +- Use **contracts**: `INPUT_KEYS` from `src/application/contracts/input_keys.ts` and `ACTIONS` from `src/data/model/action_types.ts`. No hardcoded action/input names. ## Adding a new action input 1. **`action.yml`**: Add the input with `description` and `default` (if any). -2. **`src/utils/constants.ts`**: Add the key to `INPUT_KEYS` (e.g. `NEW_INPUT: 'new-input'`). +2. **`src/application/contracts/input_keys.ts`**: Add the key to `INPUT_KEYS` (e.g. `NEW_INPUT: 'new-input'`). 3. **`src/actions/github_action.ts`**: Read the input (e.g. `core.getInput(INPUT_KEYS.NEW_INPUT)`) and pass it into the object used to build `Execution`. 4. **Optional**: If the CLI must support it, add to `local_action.ts` and the corresponding CLI option. diff --git a/_agent/docs/project-context.md b/_agent/docs/project-context.md index 2d9b7425..ae1569c4 100644 --- a/_agent/docs/project-context.md +++ b/_agent/docs/project-context.md @@ -9,7 +9,7 @@ description: Copilot – quick read, commands, and where to find more - **What it is**: GitHub Action + CLI that automates Git-Flow: creates branches from issue labels, links issues/PRs to projects, tracks commits; AI via OpenCode (progress, errors, PR descriptions). - **Entry points**: GitHub Action → `src/actions/github_action.ts`; CLI → `src/cli.ts`. Shared logic in `src/actions/common_action.ts` (single actions vs issue/PR/push). -- **Do**: Use Node 24 and pnpm, run from repo root, and preserve the dependency direction documented in `docs/dependency-rules.md`. Use `INPUT_KEYS`/`ACTIONS` and the existing logger. When adding inputs, update `action.yml`, `constants.ts`, the relevant runtime input adapter, tests, and user documentation. +- **Do**: Use Node 24 and pnpm, run from repo root, and preserve the dependency direction documented in `docs/dependency-rules.md`. Use `INPUT_KEYS`/`ACTIONS` and the existing logger. When adding inputs, update `action.yml`, `src/application/contracts/input_keys.ts`, the relevant runtime input adapter, tests, and user documentation. - **Don’t**: Edit or depend on `build/` (generated by `ncc`); run tests/lint on `build/`. ## Commands (repo root) @@ -36,5 +36,5 @@ pnpm run lint:fix ## Other rules -- **Architecture & paths**: see `architecture.md`, then the authoritative `docs/repository-architecture.md`, `docs/capability-map.md`, and `docs/dependency-rules.md`. +- **Architecture & paths**: see `architecture.md`, then the authoritative `docs/development/architecture.mdx` and `docs/dependency-rules.md`. - **Code conventions**: see `code-conventions.md` (logger, constants, adding inputs, ncc). diff --git a/_agent/docs/usecase-flows.md b/_agent/docs/usecase-flows.md index 1bbde24b..6a809ff8 100644 --- a/_agent/docs/usecase-flows.md +++ b/_agent/docs/usecase-flows.md @@ -124,7 +124,9 @@ Invoked when: | `detect_potential_problems_action` | DetectPotentialProblemsUseCase | | `recommend_steps_action` | RecommendStepsUseCase | -(Action names in constants: check_progress_action, detect_potential_problems_action, recommend_steps_action.) +(Action names are defined in `src/data/model/action_types.ts`; examples include +`check_progress_action`, `detect_potential_problems_action`, and +`recommend_steps_action`.) --- diff --git a/action.yml b/action.yml index 2fb4baba..aba12899 100644 --- a/action.yml +++ b/action.yml @@ -20,6 +20,9 @@ inputs: single-action-changelog: description: "Changelog target for executing single action." default: "" + inactivity-threshold-hours: + description: "Hours without issue activity before the scheduled inactivity action closes a waiting issue." + default: "168" emoji-labeled-title: description: "Enable titles with emojis based on issue labels." default: "true" diff --git a/build/cli/index.js b/build/cli/index.js index ad5ad5a6..6f8c3ccb 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -54477,15 +54477,15 @@ function buildAgentTasks(values, environment = process.env) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildAgentTasksFromInputs = buildAgentTasksFromInputs; exports.buildAgentTasksFromValues = buildAgentTasksFromValues; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const agent_configuration_builder_1 = __nccwpck_require__(81248); const agent_1 = __nccwpck_require__(89040); function buildAgentTasksFromInputs(read) { - const provider = read(constants_1.INPUT_KEYS.AGENT_PROVIDER)?.trim() || agent_1.DEFAULT_AGENT_PROVIDER; - const modelProvider = read(constants_1.INPUT_KEYS.AGENT_MODEL_PROVIDER)?.trim() || agent_1.DEFAULT_MODEL_PROVIDER; - const model = read(constants_1.INPUT_KEYS.AGENT_MODEL)?.trim() || agent_1.DEFAULT_AGENT_MODEL; - const effort = read(constants_1.INPUT_KEYS.AGENT_EFFORT) ?? ''; - const command = read(constants_1.INPUT_KEYS.AGENT_COMMAND) ?? ''; + const provider = read(input_keys_1.INPUT_KEYS.AGENT_PROVIDER)?.trim() || agent_1.DEFAULT_AGENT_PROVIDER; + const modelProvider = read(input_keys_1.INPUT_KEYS.AGENT_MODEL_PROVIDER)?.trim() || agent_1.DEFAULT_MODEL_PROVIDER; + const model = read(input_keys_1.INPUT_KEYS.AGENT_MODEL)?.trim() || agent_1.DEFAULT_AGENT_MODEL; + const effort = read(input_keys_1.INPUT_KEYS.AGENT_EFFORT) ?? ''; + const command = read(input_keys_1.INPUT_KEYS.AGENT_COMMAND) ?? ''; const role = (name) => ({ provider: read(`${name}-provider`), modelProvider: read(`${name}-model-provider`), @@ -54500,18 +54500,18 @@ function buildAgentTasksFromInputs(read) { effort, command, findings: { - provider: read(constants_1.INPUT_KEYS.FINDINGS_PROVIDER), - modelProvider: read(constants_1.INPUT_KEYS.FINDINGS_MODEL_PROVIDER), - model: read(constants_1.INPUT_KEYS.FINDINGS_MODEL), - effort: read(constants_1.INPUT_KEYS.FINDINGS_EFFORT), - command: read(constants_1.INPUT_KEYS.FINDINGS_COMMAND), + provider: read(input_keys_1.INPUT_KEYS.FINDINGS_PROVIDER), + modelProvider: read(input_keys_1.INPUT_KEYS.FINDINGS_MODEL_PROVIDER), + model: read(input_keys_1.INPUT_KEYS.FINDINGS_MODEL), + effort: read(input_keys_1.INPUT_KEYS.FINDINGS_EFFORT), + command: read(input_keys_1.INPUT_KEYS.FINDINGS_COMMAND), }, fixer: { - provider: read(constants_1.INPUT_KEYS.FIXER_PROVIDER), - modelProvider: read(constants_1.INPUT_KEYS.FIXER_MODEL_PROVIDER), - model: read(constants_1.INPUT_KEYS.FIXER_MODEL), - effort: read(constants_1.INPUT_KEYS.FIXER_EFFORT), - command: read(constants_1.INPUT_KEYS.FIXER_COMMAND), + provider: read(input_keys_1.INPUT_KEYS.FIXER_PROVIDER), + modelProvider: read(input_keys_1.INPUT_KEYS.FIXER_MODEL_PROVIDER), + model: read(input_keys_1.INPUT_KEYS.FIXER_MODEL), + effort: read(input_keys_1.INPUT_KEYS.FIXER_EFFORT), + command: read(input_keys_1.INPUT_KEYS.FIXER_COMMAND), }, planner: role('planner'), reviewer: role('reviewer'), @@ -54672,6 +54672,192 @@ function buildImages(values) { } +/***/ }), + +/***/ 14387: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_IMAGE_CONFIG = void 0; +/** Default illustration URLs used when an action does not receive custom images. */ +exports.DEFAULT_IMAGE_CONFIG = { + issue: { + automatic: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp" + ], + feature: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", + ], + bugfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" + ], + hotfix: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" + ], + release: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", + ], + docs: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", + ], + chore: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", + ], + }, + pullRequest: { + automatic: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + ], + feature: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", + ], + bugfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", + ], + hotfix: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", + ], + release: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", + ], + docs: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", + ], + chore: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", + ], + }, + commit: { + automatic: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + feature: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + bugfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + hotfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + release: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + docs: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + chore: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ] + } +}; + + /***/ }), /***/ 20236: @@ -54696,36 +54882,37 @@ function buildExecution(components) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildImageConfiguration = buildImageConfiguration; -const constants_1 = __nccwpck_require__(15415); +const default_image_config_1 = __nccwpck_require__(14387); +const input_keys_1 = __nccwpck_require__(88539); const input_boolean_policy_1 = __nccwpck_require__(18330); const input_values_policy_1 = __nccwpck_require__(68841); const imageInputKeys = { issue: { - automatic: constants_1.INPUT_KEYS.IMAGES_ISSUE_AUTOMATIC, - feature: constants_1.INPUT_KEYS.IMAGES_ISSUE_FEATURE, - bugfix: constants_1.INPUT_KEYS.IMAGES_ISSUE_BUGFIX, - release: constants_1.INPUT_KEYS.IMAGES_ISSUE_RELEASE, - hotfix: constants_1.INPUT_KEYS.IMAGES_ISSUE_HOTFIX, - docs: constants_1.INPUT_KEYS.IMAGES_ISSUE_DOCS, - chore: constants_1.INPUT_KEYS.IMAGES_ISSUE_CHORE, + automatic: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_AUTOMATIC, + feature: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_FEATURE, + bugfix: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_BUGFIX, + release: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_RELEASE, + hotfix: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_HOTFIX, + docs: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_DOCS, + chore: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_CHORE, }, pullRequest: { - automatic: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_AUTOMATIC, - feature: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_FEATURE, - bugfix: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_BUGFIX, - release: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_RELEASE, - hotfix: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_HOTFIX, - docs: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_DOCS, - chore: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_CHORE, + automatic: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_AUTOMATIC, + feature: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_FEATURE, + bugfix: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_BUGFIX, + release: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_RELEASE, + hotfix: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_HOTFIX, + docs: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_DOCS, + chore: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_CHORE, }, commit: { - automatic: constants_1.INPUT_KEYS.IMAGES_COMMIT_AUTOMATIC, - feature: constants_1.INPUT_KEYS.IMAGES_COMMIT_FEATURE, - bugfix: constants_1.INPUT_KEYS.IMAGES_COMMIT_BUGFIX, - release: constants_1.INPUT_KEYS.IMAGES_COMMIT_RELEASE, - hotfix: constants_1.INPUT_KEYS.IMAGES_COMMIT_HOTFIX, - docs: constants_1.INPUT_KEYS.IMAGES_COMMIT_DOCS, - chore: constants_1.INPUT_KEYS.IMAGES_COMMIT_CHORE, + automatic: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_AUTOMATIC, + feature: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_FEATURE, + bugfix: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_BUGFIX, + release: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_RELEASE, + hotfix: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_HOTFIX, + docs: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_DOCS, + chore: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_CHORE, }, }; function buildImageConfiguration(read) { @@ -54736,14 +54923,14 @@ function buildImageConfiguration(read) { const configured = (0, input_values_policy_1.parseDelimitedValues)(read(imageInputKeys[group][variant])); variants[variant] = configured.length > 0 ? configured - : [...constants_1.DEFAULT_IMAGE_CONFIG[group][variant]]; + : [...default_image_config_1.DEFAULT_IMAGE_CONFIG[group][variant]]; } groups[group] = variants; } return { - onIssue: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.IMAGES_ON_ISSUE)), - onPullRequest: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.IMAGES_ON_PULL_REQUEST)), - onCommit: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.IMAGES_ON_COMMIT)), + onIssue: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.IMAGES_ON_ISSUE)), + onPullRequest: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.IMAGES_ON_PULL_REQUEST)), + onCommit: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.IMAGES_ON_COMMIT)), ...groups, }; } @@ -54888,7 +55075,8 @@ exports.readLocalProjectConfiguration = readLocalProjectConfiguration; exports.readLocalLabelsAndIssueTypes = readLocalLabelsAndIssueTypes; exports.readLocalWorkflowConfiguration = readLocalWorkflowConfiguration; const locale_1 = __nccwpck_require__(9832); -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); +const input_keys_1 = __nccwpck_require__(88539); const input_boolean_policy_1 = __nccwpck_require__(18330); const action_input_source_1 = __nccwpck_require__(98143); const project_details_loader_1 = __nccwpck_require__(73448); @@ -54897,41 +55085,43 @@ const input_values_policy_1 = __nccwpck_require__(68841); const agent_input_builder_1 = __nccwpck_require__(71404); const image_configuration_builder_1 = __nccwpck_require__(9246); const pull_request_description_1 = __nccwpck_require__(45315); +const issue_inactivity_1 = __nccwpck_require__(38572); function input(additionalParams, actionInputs, key) { return (0, action_input_source_1.resolveActionInput)(additionalParams, actionInputs, key); } function readLocalCoreConfiguration(additionalParams, actionInputs) { return { actionInputs, - debug: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.DEBUG)), - welcomeTitle: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.WELCOME_TITLE), - welcomeMessages: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.WELCOME_MESSAGES), - singleAction: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.SINGLE_ACTION), - singleActionIssue: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE), - singleActionVersion: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION), - singleActionTitle: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.SINGLE_ACTION_TITLE), - singleActionChangelog: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG), - token: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.TOKEN), + debug: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.DEBUG)), + welcomeTitle: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.WELCOME_TITLE), + welcomeMessages: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.WELCOME_MESSAGES), + singleAction: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION), + singleActionIssue: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE), + singleActionVersion: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION), + singleActionTitle: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_TITLE), + singleActionChangelog: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG), + inactivityThresholdHours: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.INACTIVITY_THRESHOLD_HOURS), issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS, issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS), + token: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.TOKEN), }; } function readLocalAgentConfiguration(additionalParams, actionInputs) { const agentTasks = (0, agent_input_builder_1.buildAgentTasksFromValues)({ ...actionInputs, ...additionalParams }); - const bugbotFixVerifyCommandsInput = input(additionalParams, actionInputs, constants_1.INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) ?? ''; - const pullRequestDescription = (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); + const bugbotFixVerifyCommandsInput = input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) ?? ''; + const pullRequestDescription = (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); return { agentTasks, agentModel: agentTasks.findings.model, aiPullRequestDescription: pullRequestDescription, aiPullRequestDescriptionMode: pullRequestDescription - ? (0, pull_request_description_1.normalizePullRequestDescriptionMode)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) + ? (0, pull_request_description_1.normalizePullRequestDescriptionMode)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) : 'disabled', - aiMembersOnly: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_MEMBERS_ONLY)), - aiIncludeReasoning: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_INCLUDE_REASONING)), - aiIgnoreFilesInput: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_IGNORE_FILES), - aiIgnoreFiles: (0, input_values_policy_1.parseDelimitedValues)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.AI_IGNORE_FILES)), - bugbotSeverity: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.BUGBOT_SEVERITY) || constants_1.BUGBOT_MIN_SEVERITY, - bugbotCommentLimitRaw: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.BUGBOT_COMMENT_LIMIT), - bugbotCommentLimit: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(input(additionalParams, actionInputs, constants_1.INPUT_KEYS.BUGBOT_COMMENT_LIMIT), constants_1.BUGBOT_MAX_COMMENTS, 200), + aiMembersOnly: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.AI_MEMBERS_ONLY)), + aiIncludeReasoning: (0, input_boolean_policy_1.isEnabledInput)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.AI_INCLUDE_REASONING)), + aiIgnoreFilesInput: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.AI_IGNORE_FILES), + aiIgnoreFiles: (0, input_values_policy_1.parseDelimitedValues)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.AI_IGNORE_FILES)), + bugbotSeverity: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.BUGBOT_SEVERITY) || bugbot_constants_1.BUGBOT_MIN_SEVERITY, + bugbotCommentLimitRaw: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.BUGBOT_COMMENT_LIMIT), + bugbotCommentLimit: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.BUGBOT_COMMENT_LIMIT), bugbot_constants_1.BUGBOT_MAX_COMMENTS, 200), bugbotFixVerifyCommandsInput, bugbotFixVerifyCommands: String(bugbotFixVerifyCommandsInput) .split(',') @@ -54940,7 +55130,7 @@ function readLocalAgentConfiguration(additionalParams, actionInputs) { }; } async function readLocalProjectConfiguration(additionalParams, actionInputs, projectRepository, token) { - const projectIdsInput = input(additionalParams, actionInputs, constants_1.INPUT_KEYS.PROJECT_IDS); + const projectIdsInput = input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.PROJECT_IDS); const projectIds = (0, input_values_policy_1.parseDelimitedValues)(projectIdsInput); const repository = additionalParams.repo; const owner = repository && typeof repository === 'object' @@ -54951,10 +55141,10 @@ async function readLocalProjectConfiguration(additionalParams, actionInputs, pro projectIdsInput, projectIds, projects, - projectColumnIssueCreated: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_CREATED), - projectColumnPullRequestCreated: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_CREATED), - projectColumnIssueInProgress: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_IN_PROGRESS), - projectColumnPullRequestInProgress: input(additionalParams, actionInputs, constants_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS), + projectColumnIssueCreated: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_CREATED), + projectColumnPullRequestCreated: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_CREATED), + projectColumnIssueInProgress: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_IN_PROGRESS), + projectColumnPullRequestInProgress: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS), }; } function readIssueType(additionalParams, actionInputs, name, description, color) { @@ -54966,53 +55156,53 @@ function readIssueType(additionalParams, actionInputs, name, description, color) } function readLocalLabelsAndIssueTypes(additionalParams, actionInputs) { const label = (key) => input(additionalParams, actionInputs, key); - const issueTypeBug = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_BUG, constants_1.INPUT_KEYS.ISSUE_TYPE_BUG_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_BUG_COLOR); - const issueTypeHotfix = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX, constants_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_COLOR); - const issueTypeFeature = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_FEATURE, constants_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_COLOR); - const issueTypeDocumentation = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION, constants_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_COLOR); - const issueTypeMaintenance = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE, constants_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_COLOR); - const issueTypeRelease = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_RELEASE, constants_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_COLOR); - const issueTypeQuestion = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_QUESTION, constants_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_COLOR); - const issueTypeHelp = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_HELP, constants_1.INPUT_KEYS.ISSUE_TYPE_HELP_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_HELP_COLOR); - const issueTypeTask = readIssueType(additionalParams, actionInputs, constants_1.INPUT_KEYS.ISSUE_TYPE_TASK, constants_1.INPUT_KEYS.ISSUE_TYPE_TASK_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_TASK_COLOR); + const issueTypeBug = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_BUG, input_keys_1.INPUT_KEYS.ISSUE_TYPE_BUG_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_BUG_COLOR); + const issueTypeHotfix = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_COLOR); + const issueTypeFeature = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_FEATURE, input_keys_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_COLOR); + const issueTypeDocumentation = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_COLOR); + const issueTypeMaintenance = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE, input_keys_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_COLOR); + const issueTypeRelease = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_RELEASE, input_keys_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_COLOR); + const issueTypeQuestion = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_QUESTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_COLOR); + const issueTypeHelp = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HELP, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HELP_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HELP_COLOR); + const issueTypeTask = readIssueType(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.ISSUE_TYPE_TASK, input_keys_1.INPUT_KEYS.ISSUE_TYPE_TASK_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_TASK_COLOR); return { labels: { - branchManagementLauncherLabel: label(constants_1.INPUT_KEYS.BRANCH_MANAGEMENT_LAUNCHER_LABEL), - bugfixLabel: label(constants_1.INPUT_KEYS.BUGFIX_LABEL), - bugLabel: label(constants_1.INPUT_KEYS.BUG_LABEL), - hotfixLabel: label(constants_1.INPUT_KEYS.HOTFIX_LABEL), - enhancementLabel: label(constants_1.INPUT_KEYS.ENHANCEMENT_LABEL), - featureLabel: label(constants_1.INPUT_KEYS.FEATURE_LABEL), - releaseLabel: label(constants_1.INPUT_KEYS.RELEASE_LABEL), - questionLabel: label(constants_1.INPUT_KEYS.QUESTION_LABEL), - helpLabel: label(constants_1.INPUT_KEYS.HELP_LABEL), - deployLabel: label(constants_1.INPUT_KEYS.DEPLOY_LABEL), - deployedLabel: label(constants_1.INPUT_KEYS.DEPLOYED_LABEL), - docsLabel: label(constants_1.INPUT_KEYS.DOCS_LABEL), - documentationLabel: label(constants_1.INPUT_KEYS.DOCUMENTATION_LABEL), - choreLabel: label(constants_1.INPUT_KEYS.CHORE_LABEL), - maintenanceLabel: label(constants_1.INPUT_KEYS.MAINTENANCE_LABEL), - priorityHighLabel: label(constants_1.INPUT_KEYS.PRIORITY_HIGH_LABEL), - priorityMediumLabel: label(constants_1.INPUT_KEYS.PRIORITY_MEDIUM_LABEL), - priorityLowLabel: label(constants_1.INPUT_KEYS.PRIORITY_LOW_LABEL), - priorityNoneLabel: label(constants_1.INPUT_KEYS.PRIORITY_NONE_LABEL), - sizeXxlLabel: label(constants_1.INPUT_KEYS.SIZE_XXL_LABEL), - sizeXlLabel: label(constants_1.INPUT_KEYS.SIZE_XL_LABEL), - sizeLLabel: label(constants_1.INPUT_KEYS.SIZE_L_LABEL), - sizeMLabel: label(constants_1.INPUT_KEYS.SIZE_M_LABEL), - sizeSLabel: label(constants_1.INPUT_KEYS.SIZE_S_LABEL), - sizeXsLabel: label(constants_1.INPUT_KEYS.SIZE_XS_LABEL), + branchManagementLauncherLabel: label(input_keys_1.INPUT_KEYS.BRANCH_MANAGEMENT_LAUNCHER_LABEL), + bugfixLabel: label(input_keys_1.INPUT_KEYS.BUGFIX_LABEL), + bugLabel: label(input_keys_1.INPUT_KEYS.BUG_LABEL), + hotfixLabel: label(input_keys_1.INPUT_KEYS.HOTFIX_LABEL), + enhancementLabel: label(input_keys_1.INPUT_KEYS.ENHANCEMENT_LABEL), + featureLabel: label(input_keys_1.INPUT_KEYS.FEATURE_LABEL), + releaseLabel: label(input_keys_1.INPUT_KEYS.RELEASE_LABEL), + questionLabel: label(input_keys_1.INPUT_KEYS.QUESTION_LABEL), + helpLabel: label(input_keys_1.INPUT_KEYS.HELP_LABEL), + deployLabel: label(input_keys_1.INPUT_KEYS.DEPLOY_LABEL), + deployedLabel: label(input_keys_1.INPUT_KEYS.DEPLOYED_LABEL), + docsLabel: label(input_keys_1.INPUT_KEYS.DOCS_LABEL), + documentationLabel: label(input_keys_1.INPUT_KEYS.DOCUMENTATION_LABEL), + choreLabel: label(input_keys_1.INPUT_KEYS.CHORE_LABEL), + maintenanceLabel: label(input_keys_1.INPUT_KEYS.MAINTENANCE_LABEL), + priorityHighLabel: label(input_keys_1.INPUT_KEYS.PRIORITY_HIGH_LABEL), + priorityMediumLabel: label(input_keys_1.INPUT_KEYS.PRIORITY_MEDIUM_LABEL), + priorityLowLabel: label(input_keys_1.INPUT_KEYS.PRIORITY_LOW_LABEL), + priorityNoneLabel: label(input_keys_1.INPUT_KEYS.PRIORITY_NONE_LABEL), + sizeXxlLabel: label(input_keys_1.INPUT_KEYS.SIZE_XXL_LABEL), + sizeXlLabel: label(input_keys_1.INPUT_KEYS.SIZE_XL_LABEL), + sizeLLabel: label(input_keys_1.INPUT_KEYS.SIZE_L_LABEL), + sizeMLabel: label(input_keys_1.INPUT_KEYS.SIZE_M_LABEL), + sizeSLabel: label(input_keys_1.INPUT_KEYS.SIZE_S_LABEL), + sizeXsLabel: label(input_keys_1.INPUT_KEYS.SIZE_XS_LABEL), lifecycle: { - aiProcessing: label(constants_1.INPUT_KEYS.STATE_AI_PROCESSING_LABEL), - planned: label(constants_1.INPUT_KEYS.STATE_PLANNED_LABEL), - inProgress: label(constants_1.INPUT_KEYS.STATE_IN_PROGRESS_LABEL), - reviewing: label(constants_1.INPUT_KEYS.STATE_REVIEWING_LABEL), - changesRequested: label(constants_1.INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), - verified: label(constants_1.INPUT_KEYS.STATE_VERIFIED_LABEL), - ready: label(constants_1.INPUT_KEYS.STATE_READY_LABEL), - blocked: label(constants_1.INPUT_KEYS.STATE_BLOCKED_LABEL), - awaitingMaintainer: label(constants_1.INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), - awaitingIssueAuthor: label(constants_1.INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), + aiProcessing: label(input_keys_1.INPUT_KEYS.STATE_AI_PROCESSING_LABEL), + planned: label(input_keys_1.INPUT_KEYS.STATE_PLANNED_LABEL), + inProgress: label(input_keys_1.INPUT_KEYS.STATE_IN_PROGRESS_LABEL), + reviewing: label(input_keys_1.INPUT_KEYS.STATE_REVIEWING_LABEL), + changesRequested: label(input_keys_1.INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), + verified: label(input_keys_1.INPUT_KEYS.STATE_VERIFIED_LABEL), + ready: label(input_keys_1.INPUT_KEYS.STATE_READY_LABEL), + blocked: label(input_keys_1.INPUT_KEYS.STATE_BLOCKED_LABEL), + awaitingMaintainer: label(input_keys_1.INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), + awaitingIssueAuthor: label(input_keys_1.INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), }, }, issueTypes: { @@ -55049,12 +55239,12 @@ function readLocalLabelsAndIssueTypes(additionalParams, actionInputs) { function readThresholds(additionalParams, actionInputs) { const read = (key, fallback) => (0, input_number_policy_1.parseIntegerInput)(input(additionalParams, actionInputs, key), fallback); const groups = { - Xxl: [constants_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_LINES, constants_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_FILES, constants_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_COMMITS, 1000, 20, 10], - Xl: [constants_1.INPUT_KEYS.SIZE_XL_THRESHOLD_LINES, constants_1.INPUT_KEYS.SIZE_XL_THRESHOLD_FILES, constants_1.INPUT_KEYS.SIZE_XL_THRESHOLD_COMMITS, 500, 10, 5], - L: [constants_1.INPUT_KEYS.SIZE_L_THRESHOLD_LINES, constants_1.INPUT_KEYS.SIZE_L_THRESHOLD_FILES, constants_1.INPUT_KEYS.SIZE_L_THRESHOLD_COMMITS, 250, 5, 3], - M: [constants_1.INPUT_KEYS.SIZE_M_THRESHOLD_LINES, constants_1.INPUT_KEYS.SIZE_M_THRESHOLD_FILES, constants_1.INPUT_KEYS.SIZE_M_THRESHOLD_COMMITS, 100, 3, 2], - S: [constants_1.INPUT_KEYS.SIZE_S_THRESHOLD_LINES, constants_1.INPUT_KEYS.SIZE_S_THRESHOLD_FILES, constants_1.INPUT_KEYS.SIZE_S_THRESHOLD_COMMITS, 50, 2, 1], - Xs: [constants_1.INPUT_KEYS.SIZE_XS_THRESHOLD_LINES, constants_1.INPUT_KEYS.SIZE_XS_THRESHOLD_FILES, constants_1.INPUT_KEYS.SIZE_XS_THRESHOLD_COMMITS, 25, 1, 1], + Xxl: [input_keys_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_LINES, input_keys_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_FILES, input_keys_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_COMMITS, 1000, 20, 10], + Xl: [input_keys_1.INPUT_KEYS.SIZE_XL_THRESHOLD_LINES, input_keys_1.INPUT_KEYS.SIZE_XL_THRESHOLD_FILES, input_keys_1.INPUT_KEYS.SIZE_XL_THRESHOLD_COMMITS, 500, 10, 5], + L: [input_keys_1.INPUT_KEYS.SIZE_L_THRESHOLD_LINES, input_keys_1.INPUT_KEYS.SIZE_L_THRESHOLD_FILES, input_keys_1.INPUT_KEYS.SIZE_L_THRESHOLD_COMMITS, 250, 5, 3], + M: [input_keys_1.INPUT_KEYS.SIZE_M_THRESHOLD_LINES, input_keys_1.INPUT_KEYS.SIZE_M_THRESHOLD_FILES, input_keys_1.INPUT_KEYS.SIZE_M_THRESHOLD_COMMITS, 100, 3, 2], + S: [input_keys_1.INPUT_KEYS.SIZE_S_THRESHOLD_LINES, input_keys_1.INPUT_KEYS.SIZE_S_THRESHOLD_FILES, input_keys_1.INPUT_KEYS.SIZE_S_THRESHOLD_COMMITS, 50, 2, 1], + Xs: [input_keys_1.INPUT_KEYS.SIZE_XS_THRESHOLD_LINES, input_keys_1.INPUT_KEYS.SIZE_XS_THRESHOLD_FILES, input_keys_1.INPUT_KEYS.SIZE_XS_THRESHOLD_COMMITS, 25, 1, 1], }; const values = Object.fromEntries(Object.entries(groups).flatMap(([name, [linesKey, filesKey, commitsKey, linesFallback, filesFallback, commitsFallback]]) => [ [`size${name}ThresholdLines`, read(linesKey, linesFallback)], @@ -55086,28 +55276,28 @@ function readLocalWorkflowConfiguration(additionalParams, actionInputs) { const read = (key) => input(additionalParams, actionInputs, key); return { imageConfiguration: (0, image_configuration_builder_1.buildImageConfiguration)((key) => additionalParams[key] ?? actionInputs[key]), - releaseWorkflow: read(constants_1.INPUT_KEYS.RELEASE_WORKFLOW), - hotfixWorkflow: read(constants_1.INPUT_KEYS.HOTFIX_WORKFLOW), - titleEmoji: read(constants_1.INPUT_KEYS.EMOJI_LABELED_TITLE) === 'true', - branchManagementEmoji: read(constants_1.INPUT_KEYS.BRANCH_MANAGEMENT_EMOJI), - issueLocale: read(constants_1.INPUT_KEYS.ISSUES_LOCALE) ?? locale_1.Locale.DEFAULT, - pullRequestLocale: read(constants_1.INPUT_KEYS.PULL_REQUESTS_LOCALE) ?? locale_1.Locale.DEFAULT, + releaseWorkflow: read(input_keys_1.INPUT_KEYS.RELEASE_WORKFLOW), + hotfixWorkflow: read(input_keys_1.INPUT_KEYS.HOTFIX_WORKFLOW), + titleEmoji: read(input_keys_1.INPUT_KEYS.EMOJI_LABELED_TITLE) === 'true', + branchManagementEmoji: read(input_keys_1.INPUT_KEYS.BRANCH_MANAGEMENT_EMOJI), + issueLocale: read(input_keys_1.INPUT_KEYS.ISSUES_LOCALE) ?? locale_1.Locale.DEFAULT, + pullRequestLocale: read(input_keys_1.INPUT_KEYS.PULL_REQUESTS_LOCALE) ?? locale_1.Locale.DEFAULT, ...readThresholds(additionalParams, actionInputs), - mainBranch: read(constants_1.INPUT_KEYS.MAIN_BRANCH), - developmentBranch: read(constants_1.INPUT_KEYS.DEVELOPMENT_BRANCH), - featureTree: read(constants_1.INPUT_KEYS.FEATURE_TREE), - bugfixTree: read(constants_1.INPUT_KEYS.BUGFIX_TREE), - hotfixTree: read(constants_1.INPUT_KEYS.HOTFIX_TREE), - releaseTree: read(constants_1.INPUT_KEYS.RELEASE_TREE), - docsTree: read(constants_1.INPUT_KEYS.DOCS_TREE), - choreTree: read(constants_1.INPUT_KEYS.CHORE_TREE), - commitPrefixBuilder: read(constants_1.INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash', - branchManagementAlways: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.BRANCH_MANAGEMENT_ALWAYS)), - reopenIssueOnPush: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.REOPEN_ISSUE_ON_PUSH)), - issueDesiredAssigneesCount: (0, input_number_policy_1.parseIntegerInput)(read(constants_1.INPUT_KEYS.DESIRED_ASSIGNEES_COUNT), 0), - pullRequestDesiredAssigneesCount: (0, input_number_policy_1.parseIntegerInput)(read(constants_1.INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), - pullRequestDesiredReviewersCount: (0, input_number_policy_1.parseIntegerInput)(read(constants_1.INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), - pullRequestMergeTimeout: (0, input_number_policy_1.parseNonNegativeIntegerInput)(read(constants_1.INPUT_KEYS.PULL_REQUEST_MERGE_TIMEOUT), 0), + mainBranch: read(input_keys_1.INPUT_KEYS.MAIN_BRANCH), + developmentBranch: read(input_keys_1.INPUT_KEYS.DEVELOPMENT_BRANCH), + featureTree: read(input_keys_1.INPUT_KEYS.FEATURE_TREE), + bugfixTree: read(input_keys_1.INPUT_KEYS.BUGFIX_TREE), + hotfixTree: read(input_keys_1.INPUT_KEYS.HOTFIX_TREE), + releaseTree: read(input_keys_1.INPUT_KEYS.RELEASE_TREE), + docsTree: read(input_keys_1.INPUT_KEYS.DOCS_TREE), + choreTree: read(input_keys_1.INPUT_KEYS.CHORE_TREE), + commitPrefixBuilder: read(input_keys_1.INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash', + branchManagementAlways: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.BRANCH_MANAGEMENT_ALWAYS)), + reopenIssueOnPush: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.REOPEN_ISSUE_ON_PUSH)), + issueDesiredAssigneesCount: (0, input_number_policy_1.parseIntegerInput)(read(input_keys_1.INPUT_KEYS.DESIRED_ASSIGNEES_COUNT), 0), + pullRequestDesiredAssigneesCount: (0, input_number_policy_1.parseIntegerInput)(read(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), + pullRequestDesiredReviewersCount: (0, input_number_policy_1.parseIntegerInput)(read(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), + pullRequestMergeTimeout: (0, input_number_policy_1.parseNonNegativeIntegerInput)(read(input_keys_1.INPUT_KEYS.PULL_REQUEST_MERGE_TIMEOUT), 0), }; } @@ -55131,9 +55321,10 @@ const configuration_builders_1 = __nccwpck_require__(19094); const branches_builder_1 = __nccwpck_require__(30085); const size_threshold_builder_1 = __nccwpck_require__(39757); function buildLocalActionExecution(configuration, additionalParams) { - const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, aiPullRequestDescription, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks, branchManagementLauncherLabel, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, } = configuration; + const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, inactivityThresholdHours, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, aiPullRequestDescription, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, agentTasks, branchManagementLauncherLabel, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, } = configuration; return (0, execution_builder_1.buildExecution)({ debug, + inactivityThresholdHours, singleAction: new single_action_1.SingleAction(singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog), commitPrefixBuilder, issue: (0, configuration_builders_1.buildIssue)(branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, additionalParams), @@ -55217,7 +55408,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.renderLocalActionResults = renderLocalActionResults; const chalk_1 = __importDefault(__nccwpck_require__(8578)); const boxen_1 = __importDefault(__nccwpck_require__(11652)); -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); function renderLocalActionResults(results) { let content = ''; @@ -55245,7 +55436,7 @@ function renderLocalActionResults(results) { margin: 1, borderStyle: 'round', borderColor: 'cyan', - title: constants_1.TITLE, + title: product_identity_1.TITLE, titleAlignment: 'center' })); } @@ -55341,7 +55532,7 @@ exports.runMainRoute = runMainRoute; const core = __importStar(__nccwpck_require__(81078)); const chalk_1 = __importDefault(__nccwpck_require__(8578)); const boxen_1 = __importDefault(__nccwpck_require__(11652)); -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); const main_run_dispatcher_1 = __nccwpck_require__(28586); const workflow_context_1 = __nccwpck_require__(55224); @@ -55396,7 +55587,7 @@ function logWelcomeMessage(execution) { margin: 1, borderStyle: 'round', borderColor: 'cyan', - title: constants_1.TITLE, + title: product_identity_1.TITLE, titleAlignment: 'center', })); } @@ -55567,142 +55758,371 @@ function resolveWorkflowIdentifier(workflowRef) { /***/ }), -/***/ 75999: -/***/ ((__unused_webpack_module, exports) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = void 0; -exports.toApplicationError = toApplicationError; -/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ -class ApplicationError extends Error { - constructor(message, kind = 'unknown', options = {}) { - super(message); - this.name = 'ApplicationError'; - this.kind = kind; - this.retryable = options.retryable ?? false; - this.cause = options.cause; - } -} -exports.ApplicationError = ApplicationError; -function toApplicationError(error, message, kind = 'unknown', options = {}) { - return error instanceof ApplicationError - ? error - : new ApplicationError(message, kind, { ...options, cause: error }); -} - - -/***/ }), - -/***/ 79966: +/***/ 88539: /***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.replaceAgentActivityLabel = replaceAgentActivityLabel; -/** Adds or removes one activity label without touching unrelated labels. */ -function replaceAgentActivityLabel(currentLabels, activityLabel, active) { - const normalizedActivityLabel = activityLabel.trim().toLowerCase(); - if (!normalizedActivityLabel) - return [...currentLabels]; - const retained = currentLabels.filter(label => label.trim().toLowerCase() !== normalizedActivityLabel); - return active ? [...retained, activityLabel] : retained; -} - - -/***/ }), - -/***/ 15375: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.shouldTrackAgentActivity = shouldTrackAgentActivity; -const agent_1 = __nccwpck_require__(89040); -/** Decides whether a route can invoke an agent for its current event. */ -function shouldTrackAgentActivity(execution, route) { - if (!hasTarget(execution)) - return false; - switch (route) { - case 'issue': - return (execution.issue.opened || execution.issue.descriptionEdited) - && isAgentReady(execution, 'planner'); - case 'issue-comment': - case 'pull-request-review-comment': - return hasComment(execution) - && (isAgentReady(execution, 'planner') - || isAgentReady(execution, 'findings') - || isAgentReady(execution, 'fixer')); - case 'pull-request': - return ['opened', 'reopened', 'edited', 'synchronize'].includes(execution.pullRequest.action) - && (isAgentReady(execution, 'reviewer') - || (execution.ai.getAiPullRequestDescription() && isAgentReady(execution, 'planner'))); - case 'push': - return execution.commit.commits.length > 0 && isAgentReady(execution, 'findings'); - case 'single-action': - return isAgentBackedSingleAction(execution); - default: - return false; - } -} -function isAgentBackedSingleAction(execution) { - if (execution.singleAction.isThinkAction || execution.singleAction.isRecommendStepsAction) { - return isAgentReady(execution, 'planner'); - } - if (execution.singleAction.isCheckProgressAction || execution.singleAction.isDetectPotentialProblemsAction) { - return isAgentReady(execution, 'findings'); - } - return false; -} -function isAgentReady(execution, task) { - return (0, agent_1.isAgentConfigurationReady)(execution.ai?.getAgentConfiguration(task)); -} -function hasComment(execution) { - return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; -} -function hasTarget(execution) { - if (['pull_request', 'pull_request_review', 'pull_request_review_comment', 'check_suite', 'workflow_run'].includes(execution.eventName)) { - return execution.pullRequest.number > 0; - } - return execution.issue.number > 0 || execution.issueNumber > 0; -} +exports.INPUT_KEYS = void 0; +/** Canonical action and CLI input vocabulary shared by input mappers. */ +exports.INPUT_KEYS = { + // Debug + DEBUG: 'debug', + // Welcome + WELCOME_TITLE: 'welcome-title', + WELCOME_MESSAGES: 'welcome-messages', + // Single action + SINGLE_ACTION: 'single-action', + SINGLE_ACTION_ISSUE: 'single-action-issue', + SINGLE_ACTION_VERSION: 'single-action-version', + SINGLE_ACTION_TITLE: 'single-action-title', + SINGLE_ACTION_CHANGELOG: 'single-action-changelog', + INACTIVITY_THRESHOLD_HOURS: 'inactivity-threshold-hours', + // Tokens + TOKEN: 'token', + QUEUE_GATE_ONLY: 'queue-gate-only', + // Agent selection + AGENT_PROVIDER: 'agent-provider', + AGENT_MODEL_PROVIDER: 'agent-model-provider', + AGENT_EFFORT: 'agent-effort', + AGENT_MODEL: 'agent-model', + AGENT_COMMAND: 'agent-command', + FINDINGS_PROVIDER: 'findings-provider', + FINDINGS_MODEL_PROVIDER: 'findings-model-provider', + FINDINGS_EFFORT: 'findings-effort', + FINDINGS_MODEL: 'findings-model', + FINDINGS_COMMAND: 'findings-command', + FIXER_PROVIDER: 'fixer-provider', + FIXER_MODEL_PROVIDER: 'fixer-model-provider', + FIXER_EFFORT: 'fixer-effort', + FIXER_MODEL: 'fixer-model', + FIXER_COMMAND: 'fixer-command', + PLANNER_PROVIDER: 'planner-provider', + PLANNER_MODEL_PROVIDER: 'planner-model-provider', + PLANNER_EFFORT: 'planner-effort', + PLANNER_MODEL: 'planner-model', + PLANNER_COMMAND: 'planner-command', + REVIEWER_PROVIDER: 'reviewer-provider', + REVIEWER_MODEL_PROVIDER: 'reviewer-model-provider', + REVIEWER_EFFORT: 'reviewer-effort', + REVIEWER_MODEL: 'reviewer-model', + REVIEWER_COMMAND: 'reviewer-command', + TESTER_PROVIDER: 'tester-provider', + TESTER_MODEL_PROVIDER: 'tester-model-provider', + TESTER_EFFORT: 'tester-effort', + TESTER_MODEL: 'tester-model', + TESTER_COMMAND: 'tester-command', + RELEASE_PROVIDER: 'release-provider', + RELEASE_MODEL_PROVIDER: 'release-model-provider', + RELEASE_EFFORT: 'release-effort', + RELEASE_MODEL: 'release-model', + RELEASE_COMMAND: 'release-command', + // AI configuration + AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', + AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', + AI_MEMBERS_ONLY: 'ai-members-only', + AI_IGNORE_FILES: 'ai-ignore-files', + AI_INCLUDE_REASONING: 'ai-include-reasoning', + BUGBOT_SEVERITY: 'bugbot-severity', + BUGBOT_COMMENT_LIMIT: 'bugbot-comment-limit', + BUGBOT_FIX_VERIFY_COMMANDS: 'bugbot-fix-verify-commands', + // Projects + PROJECT_IDS: 'project-ids', + PROJECT_COLUMN_ISSUE_CREATED: 'project-column-issue-created', + PROJECT_COLUMN_PULL_REQUEST_CREATED: 'project-column-pull-request-created', + PROJECT_COLUMN_ISSUE_IN_PROGRESS: 'project-column-issue-in-progress', + PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: 'project-column-pull-request-in-progress', + // Images + IMAGES_ON_ISSUE: 'images-on-issue', + IMAGES_ON_PULL_REQUEST: 'images-on-pull-request', + IMAGES_ON_COMMIT: 'images-on-commit', + IMAGES_ISSUE_AUTOMATIC: 'images-issue-automatic', + IMAGES_ISSUE_FEATURE: 'images-issue-feature', + IMAGES_ISSUE_BUGFIX: 'images-issue-bugfix', + IMAGES_ISSUE_DOCS: 'images-issue-docs', + IMAGES_ISSUE_CHORE: 'images-issue-chore', + IMAGES_ISSUE_RELEASE: 'images-issue-release', + IMAGES_ISSUE_HOTFIX: 'images-issue-hotfix', + IMAGES_PULL_REQUEST_AUTOMATIC: 'images-pull-request-automatic', + IMAGES_PULL_REQUEST_FEATURE: 'images-pull-request-feature', + IMAGES_PULL_REQUEST_BUGFIX: 'images-pull-request-bugfix', + IMAGES_PULL_REQUEST_RELEASE: 'images-pull-request-release', + IMAGES_PULL_REQUEST_HOTFIX: 'images-pull-request-hotfix', + IMAGES_PULL_REQUEST_DOCS: 'images-pull-request-docs', + IMAGES_PULL_REQUEST_CHORE: 'images-pull-request-chore', + IMAGES_COMMIT_AUTOMATIC: 'images-commit-automatic', + IMAGES_COMMIT_FEATURE: 'images-commit-feature', + IMAGES_COMMIT_BUGFIX: 'images-commit-bugfix', + IMAGES_COMMIT_RELEASE: 'images-commit-release', + IMAGES_COMMIT_HOTFIX: 'images-commit-hotfix', + IMAGES_COMMIT_DOCS: 'images-commit-docs', + IMAGES_COMMIT_CHORE: 'images-commit-chore', + // Workflows + RELEASE_WORKFLOW: 'release-workflow', + HOTFIX_WORKFLOW: 'hotfix-workflow', + // Emoji + EMOJI_LABELED_TITLE: 'emoji-labeled-title', + BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', + // Labels + BRANCH_MANAGEMENT_LAUNCHER_LABEL: 'branch-management-launcher-label', + BUGFIX_LABEL: 'bugfix-label', + BUG_LABEL: 'bug-label', + HOTFIX_LABEL: 'hotfix-label', + ENHANCEMENT_LABEL: 'enhancement-label', + FEATURE_LABEL: 'feature-label', + RELEASE_LABEL: 'release-label', + QUESTION_LABEL: 'question-label', + HELP_LABEL: 'help-label', + DEPLOY_LABEL: 'deploy-label', + DEPLOYED_LABEL: 'deployed-label', + DOCS_LABEL: 'docs-label', + DOCUMENTATION_LABEL: 'documentation-label', + CHORE_LABEL: 'chore-label', + MAINTENANCE_LABEL: 'maintenance-label', + PRIORITY_HIGH_LABEL: 'priority-high-label', + PRIORITY_MEDIUM_LABEL: 'priority-medium-label', + PRIORITY_LOW_LABEL: 'priority-low-label', + PRIORITY_NONE_LABEL: 'priority-none-label', + SIZE_XXL_LABEL: 'size-xxl-label', + SIZE_XL_LABEL: 'size-xl-label', + SIZE_L_LABEL: 'size-l-label', + SIZE_M_LABEL: 'size-m-label', + SIZE_S_LABEL: 'size-s-label', + SIZE_XS_LABEL: 'size-xs-label', + // Lifecycle label inputs + STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', + STATE_PLANNED_LABEL: 'state-planned-label', + STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', + STATE_REVIEWING_LABEL: 'state-reviewing-label', + STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', + STATE_VERIFIED_LABEL: 'state-verified-label', + STATE_READY_LABEL: 'state-ready-label', + STATE_BLOCKED_LABEL: 'state-blocked-label', + STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', + STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', + // Issue Types + ISSUE_TYPE_BUG: 'issue-type-bug', + ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', + ISSUE_TYPE_BUG_COLOR: 'issue-type-bug-color', + ISSUE_TYPE_HOTFIX: 'issue-type-hotfix', + ISSUE_TYPE_HOTFIX_DESCRIPTION: 'issue-type-hotfix-description', + ISSUE_TYPE_HOTFIX_COLOR: 'issue-type-hotfix-color', + ISSUE_TYPE_FEATURE: 'issue-type-feature', + ISSUE_TYPE_FEATURE_DESCRIPTION: 'issue-type-feature-description', + ISSUE_TYPE_FEATURE_COLOR: 'issue-type-feature-color', + ISSUE_TYPE_DOCUMENTATION: 'issue-type-documentation', + ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: 'issue-type-documentation-description', + ISSUE_TYPE_DOCUMENTATION_COLOR: 'issue-type-documentation-color', + ISSUE_TYPE_MAINTENANCE: 'issue-type-maintenance', + ISSUE_TYPE_MAINTENANCE_DESCRIPTION: 'issue-type-maintenance-description', + ISSUE_TYPE_MAINTENANCE_COLOR: 'issue-type-maintenance-color', + ISSUE_TYPE_RELEASE: 'issue-type-release', + ISSUE_TYPE_RELEASE_DESCRIPTION: 'issue-type-release-description', + ISSUE_TYPE_RELEASE_COLOR: 'issue-type-release-color', + ISSUE_TYPE_QUESTION: 'issue-type-question', + ISSUE_TYPE_QUESTION_DESCRIPTION: 'issue-type-question-description', + ISSUE_TYPE_QUESTION_COLOR: 'issue-type-question-color', + ISSUE_TYPE_HELP: 'issue-type-help', + ISSUE_TYPE_HELP_DESCRIPTION: 'issue-type-help-description', + ISSUE_TYPE_HELP_COLOR: 'issue-type-help-color', + ISSUE_TYPE_TASK: 'issue-type-task', + ISSUE_TYPE_TASK_DESCRIPTION: 'issue-type-task-description', + ISSUE_TYPE_TASK_COLOR: 'issue-type-task-color', + // Locale + ISSUES_LOCALE: 'issues-locale', + PULL_REQUESTS_LOCALE: 'pull-requests-locale', + // Size Thresholds + SIZE_XXL_THRESHOLD_LINES: 'size-xxl-threshold-lines', + SIZE_XXL_THRESHOLD_FILES: 'size-xxl-threshold-files', + SIZE_XXL_THRESHOLD_COMMITS: 'size-xxl-threshold-commits', + SIZE_XL_THRESHOLD_LINES: 'size-xl-threshold-lines', + SIZE_XL_THRESHOLD_FILES: 'size-xl-threshold-files', + SIZE_XL_THRESHOLD_COMMITS: 'size-xl-threshold-commits', + SIZE_L_THRESHOLD_LINES: 'size-l-threshold-lines', + SIZE_L_THRESHOLD_FILES: 'size-l-threshold-files', + SIZE_L_THRESHOLD_COMMITS: 'size-l-threshold-commits', + SIZE_M_THRESHOLD_LINES: 'size-m-threshold-lines', + SIZE_M_THRESHOLD_FILES: 'size-m-threshold-files', + SIZE_M_THRESHOLD_COMMITS: 'size-m-threshold-commits', + SIZE_S_THRESHOLD_LINES: 'size-s-threshold-lines', + SIZE_S_THRESHOLD_FILES: 'size-s-threshold-files', + SIZE_S_THRESHOLD_COMMITS: 'size-s-threshold-commits', + SIZE_XS_THRESHOLD_LINES: 'size-xs-threshold-lines', + SIZE_XS_THRESHOLD_FILES: 'size-xs-threshold-files', + SIZE_XS_THRESHOLD_COMMITS: 'size-xs-threshold-commits', + // Branches + MAIN_BRANCH: 'main-branch', + DEVELOPMENT_BRANCH: 'development-branch', + FEATURE_TREE: 'feature-tree', + BUGFIX_TREE: 'bugfix-tree', + HOTFIX_TREE: 'hotfix-tree', + RELEASE_TREE: 'release-tree', + DOCS_TREE: 'docs-tree', + CHORE_TREE: 'chore-tree', + // Commit + COMMIT_PREFIX_TRANSFORMS: 'commit-prefix-transforms', + // Issue + BRANCH_MANAGEMENT_ALWAYS: 'branch-management-always', + REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', + DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', + // Pull Request + PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', + PULL_REQUEST_DESIRED_REVIEWERS_COUNT: 'desired-reviewers-count', + PULL_REQUEST_MERGE_TIMEOUT: 'merge-timeout', +}; /***/ }), -/***/ 15044: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { +/***/ 18739: +/***/ ((__unused_webpack_module, exports) => { "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.TITLE = void 0; +exports.TITLE = 'Copilot'; + + +/***/ }), + +/***/ 75999: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ApplicationError = void 0; +exports.toApplicationError = toApplicationError; +/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ +class ApplicationError extends Error { + constructor(message, kind = 'unknown', options = {}) { + super(message); + this.name = 'ApplicationError'; + this.kind = kind; + this.retryable = options.retryable ?? false; + this.cause = options.cause; + } +} +exports.ApplicationError = ApplicationError; +function toApplicationError(error, message, kind = 'unknown', options = {}) { + return error instanceof ApplicationError + ? error + : new ApplicationError(message, kind, { ...options, cause: error }); +} + + +/***/ }), + +/***/ 79966: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.replaceAgentActivityLabel = replaceAgentActivityLabel; +/** Adds or removes one activity label without touching unrelated labels. */ +function replaceAgentActivityLabel(currentLabels, activityLabel, active) { + const normalizedActivityLabel = activityLabel.trim().toLowerCase(); + if (!normalizedActivityLabel) + return [...currentLabels]; + const retained = currentLabels.filter(label => label.trim().toLowerCase() !== normalizedActivityLabel); + return active ? [...retained, activityLabel] : retained; +} + + +/***/ }), + +/***/ 15375: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.shouldTrackAgentActivity = shouldTrackAgentActivity; +const agent_1 = __nccwpck_require__(89040); +/** Decides whether a route can invoke an agent for its current event. */ +function shouldTrackAgentActivity(execution, route) { + if (!hasTarget(execution)) + return false; + switch (route) { + case 'issue': + return (execution.issue.opened || execution.issue.descriptionEdited) + && isAgentReady(execution, 'planner'); + case 'issue-comment': + case 'pull-request-review-comment': + return hasComment(execution) + && (isAgentReady(execution, 'planner') + || isAgentReady(execution, 'findings') + || isAgentReady(execution, 'fixer')); + case 'pull-request': + return ['opened', 'reopened', 'edited', 'synchronize'].includes(execution.pullRequest.action) + && (isAgentReady(execution, 'reviewer') + || (execution.ai.getAiPullRequestDescription() && isAgentReady(execution, 'planner'))); + case 'push': + return execution.commit.commits.length > 0 && isAgentReady(execution, 'findings'); + case 'single-action': + return isAgentBackedSingleAction(execution); + default: + return false; + } +} +function isAgentBackedSingleAction(execution) { + if (execution.singleAction.isThinkAction || execution.singleAction.isRecommendStepsAction) { + return isAgentReady(execution, 'planner'); + } + if (execution.singleAction.isCheckProgressAction || execution.singleAction.isDetectPotentialProblemsAction) { + return isAgentReady(execution, 'findings'); + } + return false; +} +function isAgentReady(execution, task) { + return (0, agent_1.isAgentConfigurationReady)(execution.ai?.getAgentConfiguration(task)); +} +function hasComment(execution) { + return (execution.issue.commentBody || execution.pullRequest.commentBody).trim().length > 0; +} +function hasTarget(execution) { + if (['pull_request', 'pull_request_review', 'pull_request_review_comment', 'check_suite', 'workflow_run'].includes(execution.eventName)) { + return execution.pullRequest.number > 0; + } + return execution.issue.number > 0 || execution.issueNumber > 0; +} + + +/***/ }), + +/***/ 15044: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; @@ -56150,6 +56570,23 @@ function findPreviousIssueBranch(branches, issueNumber, branchTypes) { } +/***/ }), + +/***/ 51389: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.BUGBOT_MIN_SEVERITY = exports.BUGBOT_MAX_COMMENTS = exports.BUGBOT_MARKER_PREFIX = void 0; +/** Hidden marker prefix used to reconcile Bugbot findings across comments. */ +exports.BUGBOT_MARKER_PREFIX = 'copilot-bugbot'; +/** Maximum number of individual Bugbot comments published for one analysis. */ +exports.BUGBOT_MAX_COMMENTS = 20; +/** Minimum severity published by default. */ +exports.BUGBOT_MIN_SEVERITY = 'low'; + + /***/ }), /***/ 53822: @@ -56778,7 +57215,7 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun /***/ }), -/***/ 56637: +/***/ 23381: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -56788,22 +57225,8 @@ exports.SETUP_FEATURE_DESCRIPTIONS = exports.SETUP_AGENT_TASKS = void 0; exports.createDefaultSetupStorageConfiguration = createDefaultSetupStorageConfiguration; exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; exports.mergeSetupConfiguration = mergeSetupConfiguration; -exports.validateSetupConfiguration = validateSetupConfiguration; -exports.buildSetupPlan = buildSetupPlan; -exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; -exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; -exports.buildSetupActionInputs = buildSetupActionInputs; -exports.resolveSetupResourceScope = resolveSetupResourceScope; -exports.getSetupResourceStoragePolicy = getSetupResourceStoragePolicy; -exports.getSetupStorageConfiguration = getSetupStorageConfiguration; -exports.resolveSetupResourceTarget = resolveSetupResourceTarget; -exports.setupResourceExists = setupResourceExists; -exports.shouldUpsertSetupResource = shouldUpsertSetupResource; -exports.validateSetupStorageAgainstRemote = validateSetupStorageAgainstRemote; -exports.usesOrganizationStorage = usesOrganizationStorage; const agent_1 = __nccwpck_require__(89040); -const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); -const pull_request_description_1 = __nccwpck_require__(45315); +const issue_inactivity_1 = __nccwpck_require__(38572); exports.SETUP_AGENT_TASKS = [ 'planner', 'findings', @@ -56822,37 +57245,10 @@ exports.SETUP_FEATURE_DESCRIPTIONS = { hotfix: 'Hotfix workflow: emergency release from a production tag', agentProvisioning: 'Agent CLI provisioning check workflow', credentialHealth: 'Read-only remote credential health workflow for setup and doctor', + inactiveIssueClosure: 'Close issues after inactivity while waiting for an issuer or issue author', issueTemplates: 'Issue templates for feature, bug, documentation, and operations', pullRequestTemplate: 'Pull request template', }; -const WORKFLOW_FILES = { - issues: ['copilot_issue.yml'], - pullRequests: ['copilot_pull_request.yml'], - commits: ['copilot_commit.yml'], - issueComments: ['copilot_issue_comment.yml'], - pullRequestComments: ['copilot_pull_request_comment.yml'], - release: ['release_workflow.yml'], - hotfix: ['hotfix_workflow.yml'], - agentProvisioning: ['agent-cli-provisioning.yml'], - credentialHealth: ['copilot_credential_health.yml'], -}; -const ISSUE_TEMPLATE_FILES = [ - 'config.yml', - 'feature_request.yml', - 'bug_report.yml', - 'doc_update.yml', - 'chore_task.yml', - 'help_request.yml', - 'hotfix.yml', - 'release.yml', -]; -const SECRET_BY_MODEL_PROVIDER = { - openai: 'OPENAI_API_KEY', - anthropic: 'ANTHROPIC_API_KEY', - google: 'GOOGLE_API_KEY', - openrouter: 'OPENROUTER_API_KEY', -}; -const RESOURCE_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/; function defaultStoragePolicy() { return { defaultScope: 'repository', @@ -56875,7 +57271,7 @@ function createDefaultSetupConfiguration() { effort: '', }); const agents = Object.fromEntries(exports.SETUP_AGENT_TASKS.map(task => [task, defaultRole()])); - const features = Object.fromEntries(Object.keys(exports.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true])); + const features = Object.fromEntries(Object.keys(exports.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, feature !== 'inactiveIssueClosure'])); return { features, agents, @@ -56893,6 +57289,7 @@ function createDefaultSetupConfiguration() { desiredAssigneesCount: 1, desiredReviewersCount: 1, mergeTimeout: 600, + inactivityThresholdHours: issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS, issueLocale: 'en-US', pullRequestLocale: 'en-US', commitPrefixTransforms: 'replace-slash', @@ -56939,81 +57336,92 @@ function mergeSetupConfiguration(base, overrides = {}) { manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, storage: { - secrets: mergeStoragePolicy(base.storage?.secrets, overrides.storage?.secrets), - variables: mergeStoragePolicy(base.storage?.variables, overrides.storage?.variables), + secrets: { + ...base.storage.secrets, + ...(overrides.storage?.secrets ?? {}), + overrides: { + ...base.storage.secrets.overrides, + ...(overrides.storage?.secrets?.overrides ?? {}), + }, + }, + variables: { + ...base.storage.variables, + ...(overrides.storage?.variables ?? {}), + overrides: { + ...base.storage.variables.overrides, + ...(overrides.storage?.variables?.overrides ?? {}), + }, + }, }, }; } -function validateSetupConfiguration(configuration) { - const errors = []; - const nonEmpty = [ - ['main branch', configuration.repository.mainBranch], - ['development branch', configuration.repository.developmentBranch], - ['feature branch prefix', configuration.repository.featureTree], - ['bugfix branch prefix', configuration.repository.bugfixTree], - ['hotfix branch prefix', configuration.repository.hotfixTree], - ['release branch prefix', configuration.repository.releaseTree], - ['docs branch prefix', configuration.repository.docsTree], - ['chore branch prefix', configuration.repository.choreTree], - ]; - for (const [name, value] of nonEmpty) { - if (!value.trim() || /\s/.test(value)) - errors.push(`The ${name} must be non-empty and contain no whitespace.`); - } - if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { - errors.push('Desired assignees must be between 0 and 10.'); - } - if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { - errors.push('Desired reviewers must be between 0 and 15.'); - } - if (configuration.repository.mergeTimeout < 0) - errors.push('Merge timeout cannot be negative.'); - if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { - errors.push('Bugbot comment limit must be between 1 and 100.'); - } - if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { - errors.push('Bugbot severity must be info, low, medium, or high.'); - } - if (configuration.ai.pullRequestDescriptionMode !== undefined - && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { - errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); - } - if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { - errors.push('Agent provisioning must be auto, always, or disabled.'); - } - errors.push(...validateStorageConfiguration(configuration.storage)); - for (const task of exports.SETUP_AGENT_TASKS) { - const agent = configuration.agents[task]; - if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) - errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); - if (!agent.modelProvider.trim() || !agent.model.trim()) - errors.push(`Model provider and model are required for ${task}.`); - if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) - errors.push(`Model provider and model for ${task} cannot contain whitespace.`); - } - return errors; -} -function buildSetupPlan(configuration) { - const workflowFiles = Object.entries(WORKFLOW_FILES) - .filter(([feature]) => configuration.features[feature] !== false) - .flatMap(([, files]) => files); - const issueTemplateFiles = configuration.features.issueTemplates === false - ? [] - : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') + + +/***/ }), + +/***/ 87770: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildSetupPlan = buildSetupPlan; +exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; +exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; +exports.buildSetupActionInputs = buildSetupActionInputs; +const pull_request_description_1 = __nccwpck_require__(45315); +const setup_configuration_defaults_1 = __nccwpck_require__(23381); +const setup_configuration_storage_policy_1 = __nccwpck_require__(2554); +const WORKFLOW_FILES = { + issues: ['copilot_issue.yml'], + pullRequests: ['copilot_pull_request.yml'], + commits: ['copilot_commit.yml'], + issueComments: ['copilot_issue_comment.yml'], + pullRequestComments: ['copilot_pull_request_comment.yml'], + release: ['release_workflow.yml'], + hotfix: ['hotfix_workflow.yml'], + agentProvisioning: ['agent-cli-provisioning.yml'], + credentialHealth: ['copilot_credential_health.yml'], + inactiveIssueClosure: ['copilot_close_inactive_issues.yml'], +}; +const ISSUE_TEMPLATE_FILES = [ + 'config.yml', + 'feature_request.yml', + 'bug_report.yml', + 'doc_update.yml', + 'chore_task.yml', + 'help_request.yml', + 'hotfix.yml', + 'release.yml', +]; +const SECRET_BY_MODEL_PROVIDER = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + google: 'GOOGLE_API_KEY', + openrouter: 'OPENROUTER_API_KEY', +}; +function buildSetupPlan(configuration) { + const workflowFiles = Object.entries(WORKFLOW_FILES) + .filter(([feature]) => configuration.features[feature] !== false) + .flatMap(([, files]) => files); + const issueTemplateFiles = configuration.features.issueTemplates === false + ? [] + : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); const selectedFiles = [ ...workflowFiles.map(file => `workflows/${file}`), ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), ]; + const credentialRequirements = buildSetupCredentialRequirements(configuration); return { configuration, workflowFiles, issueTemplateFiles, selectedFiles, variables: buildSetupRepositoryVariables(configuration), - requiredSecrets: buildRequiredSetupSecrets(configuration), - credentialRequirements: buildSetupCredentialRequirements(configuration), + requiredSecrets: credentialRequirements.map(requirement => requirement.name), + credentialRequirements, warnings: buildSetupWarnings(configuration), }; } @@ -57025,7 +57433,7 @@ function buildSetupCredentialRequirements(configuration) { requirements.set(name, { name, kind, description, provider, model }); }; add('PAT', 'workflowPat', 'A separate GitHub token owned by the bot account. It is used by workflows at runtime.'); - for (const task of exports.SETUP_AGENT_TASKS) { + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; if (agent.provider === 'cursor') { add('CURSOR_API_KEY', 'apiKey', 'Cursor API key used by the Cursor agent runtime.', 'cursor', agent.model); @@ -57056,9 +57464,9 @@ function buildSetupRepositoryVariables(configuration) { add('AGENT_MODEL', base.model); add('AGENT_EFFORT', base.effort); add('AGENT_PROVISIONING', configuration.ai.provisioningMode); - add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(exports.SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); - add('AGENT_ALLOWED_MODELS', unique(exports.SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); - for (const task of exports.SETUP_AGENT_TASKS) { + add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(setup_configuration_defaults_1.SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); + add('AGENT_ALLOWED_MODELS', unique(setup_configuration_defaults_1.SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const prefix = task.toUpperCase(); const agent = configuration.agents[task]; add(`${prefix}_PROVIDER`, agent.provider); @@ -57080,6 +57488,9 @@ function buildSetupRepositoryVariables(configuration) { add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); add('MERGE_TIMEOUT', repository.mergeTimeout); + if (configuration.features.inactiveIssueClosure !== false) { + add('INACTIVITY_THRESHOLD_HOURS', repository.inactivityThresholdHours); + } add('ISSUES_LOCALE', repository.issueLocale); add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); @@ -57116,6 +57527,7 @@ function buildSetupActionInputs(configuration) { 'desired-assignees-count': String(repository.desiredAssigneesCount), 'desired-reviewers-count': String(repository.desiredReviewersCount), 'merge-timeout': String(repository.mergeTimeout), + 'inactivity-threshold-hours': String(repository.inactivityThresholdHours), 'issues-locale': repository.issueLocale, 'pull-requests-locale': repository.pullRequestLocale, 'commit-prefix-transforms': repository.commitPrefixTransforms, @@ -57145,7 +57557,7 @@ function buildAgentActionInputs(configuration) { add('agent-model-provider', base.modelProvider); add('agent-model', base.model); add('agent-effort', base.effort); - for (const task of exports.SETUP_AGENT_TASKS) { + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; const prefix = `${task}-`; add(`${prefix}provider`, agent.provider); @@ -57155,9 +57567,6 @@ function buildAgentActionInputs(configuration) { } return result; } -function buildRequiredSetupSecrets(configuration) { - return buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); -} function buildSetupWarnings(configuration) { const warnings = []; if (configuration.features.release !== false && configuration.features.hotfix !== false) { @@ -57166,17 +57575,72 @@ function buildSetupWarnings(configuration) { if (configuration.ai.provisioningMode === 'always') { warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); } + if (configuration.features.inactiveIssueClosure !== false) { + warnings.push('Inactive issue closure is enabled; waiting issues are closed after the configured inactivity threshold and can be reopened with a new comment.'); + } if (configuration.projects.ids.trim()) { warnings.push('Project IDs must be accessible to the PAT and use the expected project column names.'); } - if (exports.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { + if (setup_configuration_defaults_1.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); } - if (usesOrganizationStorage(configuration)) { + if ((0, setup_configuration_storage_policy_1.usesOrganizationStorage)(configuration)) { warnings.push('Organization-level Secrets and Variables require organization permissions; selected access is the safest default and repository values take precedence.'); } return warnings; } +function unique(values) { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} + + +/***/ }), + +/***/ 56637: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +/** Public setup-policy boundary. Each concern is implemented in a focused policy module. */ +__exportStar(__nccwpck_require__(23381), exports); +__exportStar(__nccwpck_require__(87770), exports); +__exportStar(__nccwpck_require__(2554), exports); +__exportStar(__nccwpck_require__(13339), exports); + + +/***/ }), + +/***/ 2554: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.resolveSetupResourceScope = resolveSetupResourceScope; +exports.getSetupResourceStoragePolicy = getSetupResourceStoragePolicy; +exports.getSetupStorageConfiguration = getSetupStorageConfiguration; +exports.resolveSetupResourceTarget = resolveSetupResourceTarget; +exports.setupResourceExists = setupResourceExists; +exports.shouldUpsertSetupResource = shouldUpsertSetupResource; +exports.validateSetupStorageAgainstRemote = validateSetupStorageAgainstRemote; +exports.usesOrganizationStorage = usesOrganizationStorage; +exports.validateStorageConfiguration = validateStorageConfiguration; +const setup_configuration_defaults_1 = __nccwpck_require__(23381); function resolveSetupResourceScope(policy, name) { return policy.overrides[name] ?? policy.defaultScope; } @@ -57184,7 +57648,7 @@ function getSetupResourceStoragePolicy(configuration, kind) { return getSetupStorageConfiguration(configuration)[kind === 'secret' ? 'secrets' : 'variables']; } function getSetupStorageConfiguration(configuration) { - const fallback = createDefaultSetupStorageConfiguration(); + const fallback = (0, setup_configuration_defaults_1.createDefaultSetupStorageConfiguration)(); return { secrets: mergeStoragePolicy(fallback.secrets, configuration.storage?.secrets), variables: mergeStoragePolicy(fallback.variables, configuration.storage?.variables), @@ -57261,17 +57725,7 @@ function usesOrganizationStorage(configuration) { const storage = getSetupStorageConfiguration(configuration); return [storage.secrets, storage.variables].some(policy => policy.defaultScope === 'organization' || Object.values(policy.overrides).includes('organization')); } -function mergeStoragePolicy(base, override) { - const fallback = base ?? defaultStoragePolicy(); - return { - ...fallback, - ...(override ?? {}), - overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, - }; -} function validateStorageConfiguration(storage) { - // Setup files created before scoped storage was introduced remain valid and - // receive the repository-level defaults through getSetupStorageConfiguration. if (!storage) return []; const errors = []; @@ -57286,7 +57740,7 @@ function validateStorageConfiguration(storage) { if (typeof policy.preserveExisting !== 'boolean') errors.push(`${kind} preserveExisting must be a boolean.`); for (const [name, scope] of Object.entries(policy.overrides ?? {})) { - if (!RESOURCE_NAME_PATTERN.test(name)) + if (!/^[A-Z][A-Z0-9_]*$/.test(name)) errors.push(`${kind} override name ${name} must be an uppercase GitHub Actions name.`); if (!['repository', 'organization'].includes(scope)) errors.push(`${kind} override ${name} must use repository or organization.`); @@ -57294,8 +57748,82 @@ function validateStorageConfiguration(storage) { } return errors; } -function unique(values) { - return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +function mergeStoragePolicy(base, override) { + const fallback = base ?? (0, setup_configuration_defaults_1.createDefaultSetupStorageConfiguration)().secrets; + return { + ...fallback, + ...(override ?? {}), + overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, + }; +} + + +/***/ }), + +/***/ 13339: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.validateSetupConfiguration = validateSetupConfiguration; +const setup_configuration_defaults_1 = __nccwpck_require__(23381); +const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); +const setup_configuration_storage_policy_1 = __nccwpck_require__(2554); +const issue_inactivity_1 = __nccwpck_require__(38572); +function validateSetupConfiguration(configuration) { + const errors = []; + const nonEmpty = [ + ['main branch', configuration.repository.mainBranch], + ['development branch', configuration.repository.developmentBranch], + ['feature branch prefix', configuration.repository.featureTree], + ['bugfix branch prefix', configuration.repository.bugfixTree], + ['hotfix branch prefix', configuration.repository.hotfixTree], + ['release branch prefix', configuration.repository.releaseTree], + ['docs branch prefix', configuration.repository.docsTree], + ['chore branch prefix', configuration.repository.choreTree], + ]; + for (const [name, value] of nonEmpty) { + if (!value.trim() || /\s/.test(value)) + errors.push(`The ${name} must be non-empty and contain no whitespace.`); + } + if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { + errors.push('Desired assignees must be between 0 and 10.'); + } + if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { + errors.push('Desired reviewers must be between 0 and 15.'); + } + if (configuration.repository.mergeTimeout < 0) + errors.push('Merge timeout cannot be negative.'); + if (!Number.isInteger(configuration.repository.inactivityThresholdHours) + || configuration.repository.inactivityThresholdHours < 1 + || configuration.repository.inactivityThresholdHours > issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS) { + errors.push(`Inactivity threshold must be between 1 and ${issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS} hours.`); + } + if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { + errors.push('Bugbot comment limit must be between 1 and 100.'); + } + if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { + errors.push('Bugbot severity must be info, low, medium, or high.'); + } + if (configuration.ai.pullRequestDescriptionMode !== undefined + && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { + errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); + } + if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { + errors.push('Agent provisioning must be auto, always, or disabled.'); + } + errors.push(...(0, setup_configuration_storage_policy_1.validateStorageConfiguration)(configuration.storage)); + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) + errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); + if (!agent.modelProvider.trim() || !agent.model.trim()) + errors.push(`Model provider and model are required for ${task}.`); + if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) + errors.push(`Model provider and model for ${task} cannot contain whitespace.`); + } + return errors; } @@ -57418,6 +57946,7 @@ exports.COPILOT_WORKFLOW_NAMES = [ 'Copilot - Commit', 'Copilot - Pull Request', 'Copilot - Pull Request Comment', + 'Copilot - Close Inactive Issues', 'Task - Hotfix', 'Task - Release', ]; @@ -57674,6 +58203,164 @@ function logProgressAssessment(progress, summary, reasoning, remaining) { } +/***/ }), + +/***/ 84579: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CloseInactiveIssuesUseCase = void 0; +const close_inactive_issues_workflow_1 = __nccwpck_require__(86288); +/** Application boundary for the scheduled inactivity-maintenance action. */ +class CloseInactiveIssuesUseCase { + constructor(issueQueryPort, issueClosurePort, clock) { + this.issueQueryPort = issueQueryPort; + this.issueClosurePort = issueClosurePort; + this.clock = clock; + this.taskId = 'CloseInactiveIssuesUseCase'; + } + async invoke(param) { + return (0, close_inactive_issues_workflow_1.runCloseInactiveIssuesWorkflow)(param, { + issueQueryPort: this.issueQueryPort, + issueClosurePort: this.issueClosurePort, + clock: this.clock, + }); + } +} +exports.CloseInactiveIssuesUseCase = CloseInactiveIssuesUseCase; + + +/***/ }), + +/***/ 86288: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.runCloseInactiveIssuesWorkflow = runCloseInactiveIssuesWorkflow; +const result_1 = __nccwpck_require__(73817); +const issue_inactivity_1 = __nccwpck_require__(38572); +const github_comment_publication_policy_1 = __nccwpck_require__(72712); +const logging_ports_1 = __nccwpck_require__(6152); +const TASK_ID = 'CloseInactiveIssuesUseCase'; +const INACTIVITY_COMMENT = (thresholdHours) => `This issue was automatically closed due to inactivity while waiting for a response. No activity was detected for at least **${thresholdHours} hours**. Reopen it and add a comment if it still needs attention.`; +/** Scans waiting issues and closes only candidates that remain inactive. */ +async function runCloseInactiveIssuesWorkflow(param, dependencies) { + const waitingLabels = unique([ + param.labels.lifecycle.awaitingMaintainer, + param.labels.lifecycle.awaitingIssueAuthor, + ]); + const activityLabel = param.labels.lifecycle.aiProcessing; + const nowMilliseconds = dependencies.clock.nowMilliseconds(); + const thresholdHours = param.inactivityThresholdHours; + try { + const candidates = await listCandidates(param, waitingLabels, dependencies.issueQueryPort); + let eligibleCount = 0; + let closedCount = 0; + let skippedCount = 0; + const errors = []; + for (const candidate of candidates) { + const initialDecision = (0, issue_inactivity_1.evaluateIssueInactivity)({ + issue: candidate, + waitingLabels, + agentActivityLabel: activityLabel, + thresholdHours, + nowMilliseconds, + }); + if (initialDecision.kind !== 'close') { + skippedCount++; + continue; + } + eligibleCount++; + try { + // Re-read both labels and updated_at immediately before the + // mutation so a comment or state transition during the scan + // invalidates the stale list snapshot. + const current = await dependencies.issueQueryPort.getOpenIssue(param.owner, param.repo, candidate.number, param.tokens.token); + if (!current || (0, issue_inactivity_1.evaluateIssueInactivity)({ + issue: current, + waitingLabels, + agentActivityLabel: activityLabel, + thresholdHours, + nowMilliseconds: dependencies.clock.nowMilliseconds(), + }).kind !== 'close') { + skippedCount++; + continue; + } + const closed = await dependencies.issueClosurePort.closeIssue(param.owner, param.repo, candidate.number, param.tokens.token); + if (!closed) { + skippedCount++; + continue; + } + closedCount++; + await dependencies.issueClosurePort.addComment(param.owner, param.repo, candidate.number, INACTIVITY_COMMENT(thresholdHours), param.tokens.token); + (0, logging_ports_1.logInfo)(`Issue #${candidate.number} closed after inactivity.`); + } + catch (error) { + const message = `Unable to close issue #${candidate.number} after inactivity.`; + (0, logging_ports_1.logError)(message); + errors.push(`${message} ${safeErrorMessage(error)}`); + } + } + (0, logging_ports_1.logDebugInfo)(`${TASK_ID}: scanned=${candidates.length}, eligible=${eligibleCount}, closed=${closedCount}, skipped=${skippedCount}.`); + return [new result_1.Result({ + id: TASK_ID, + success: errors.length === 0, + executed: closedCount > 0 || eligibleCount > 0, + steps: buildSteps(candidates.length, closedCount, skippedCount), + payload: { + scanned: candidates.length, + eligible: eligibleCount, + closed: closedCount, + skipped: skippedCount, + }, + errors, + })]; + } + catch (error) { + const message = 'Unable to scan issues for inactivity closure.'; + (0, logging_ports_1.logError)(message); + return [new result_1.Result({ + id: TASK_ID, + success: false, + executed: true, + steps: [message], + errors: [`${message} ${safeErrorMessage(error)}`], + })]; + } +} +async function listCandidates(param, waitingLabels, queryPort) { + const candidates = []; + for (const label of waitingLabels) { + candidates.push(...await queryPort.listOpenIssuesByLabel(param.owner, param.repo, label, param.tokens.token)); + } + const uniqueCandidates = new Map(); + for (const candidate of candidates) + uniqueCandidates.set(candidate.number, candidate); + return [...uniqueCandidates.values()]; +} +function buildSteps(scanned, closed, skipped) { + const steps = [`Scanned ${scanned} open issue(s) waiting for a response.`]; + if (closed > 0) + steps.push(`Closed ${closed} issue(s) after the inactivity threshold.`); + if (skipped > 0) + steps.push(`Skipped ${skipped} candidate(s) because they were no longer eligible.`); + if (closed === 0) + steps.push('No issue was closed for inactivity.'); + return steps; +} +function unique(values) { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} +function safeErrorMessage(error) { + const message = (0, github_comment_publication_policy_1.sanitizePublishedError)(error instanceof Error ? error.message : error); + return message || 'Unknown provider error.'; +} + + /***/ }), /***/ 76549: @@ -57685,19 +58372,19 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.validateReleaseInput = validateReleaseInput; exports.normalizeVersion = normalizeVersion; exports.versionForRelease = versionForRelease; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const application_error_1 = __nccwpck_require__(75999); const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/; function validateReleaseInput(input) { if (!input.version.length) - return `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`; + return `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`; if (!input.title.length) - return `${constants_1.INPUT_KEYS.SINGLE_ACTION_TITLE} is not set.`; + return `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_TITLE} is not set.`; if (!input.changelog.length) - return `${constants_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`; + return `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`; const normalized = normalizeVersion(input.version); return normalized === undefined - ? `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0). Got: ${input.version}` + ? `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0). Got: ${input.version}` : undefined; } function normalizeVersion(version) { @@ -57825,7 +58512,7 @@ exports.CreateTagUseCase = CreateTagUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runCreateTag = runCreateTag; const result_1 = __nccwpck_require__(73817); -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); async function runCreateTag(param, taskId, repositoryTagPort) { const validationFailure = validateTagInput(param, taskId); @@ -57845,7 +58532,7 @@ async function runCreateTag(param, taskId, repositoryTagPort) { function validateTagInput(param, taskId) { if (param.singleAction.version.length === 0) { (0, logging_ports_1.logError)('Version is not set.'); - return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); } if (param.currentConfiguration.releaseBranch === undefined) { (0, logging_ports_1.logError)('Working branch not found in configuration.'); @@ -58033,6 +58720,37 @@ async function findIssueBranch(param, repository) { } +/***/ }), + +/***/ 57389: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createInitialSetupRequest = createInitialSetupRequest; +/** Converts the legacy execution aggregate into the setup use case's explicit request. */ +function createInitialSetupRequest(execution) { + return { + owner: execution.owner, + repo: execution.repo, + token: execution.tokens.token, + labels: execution.labels, + issueTypes: execution.issueTypes, + setupConfiguration: asObject(execution.inputs?.setupConfiguration), + setupCredentials: asObject(execution.inputs?.setupCredentials), + setupRemoteConfiguration: asObject(execution.inputs?.setupRemoteConfiguration), + workflowUpdates: asStringArray(execution.inputs?.setupWorkflowUpdates), + }; +} +function asObject(value) { + return value && typeof value === 'object' ? value : undefined; +} +function asStringArray(value) { + return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []; +} + + /***/ }), /***/ 84837: @@ -58043,6 +58761,7 @@ async function findIssueBranch(param, repository) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.InitialSetupUseCase = void 0; const initial_setup_workflow_1 = __nccwpck_require__(18079); +const initial_setup_request_1 = __nccwpck_require__(57389); /** Application boundary for provisioning a repository for Copilot automation. */ class InitialSetupUseCase { constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort, setupRemoteConfigurationReadPort) { @@ -58059,7 +58778,7 @@ class InitialSetupUseCase { this.taskId = 'InitialSetupUseCase'; } async invoke(param) { - return await (0, initial_setup_workflow_1.runInitialSetupWorkflow)(param, { + return await (0, initial_setup_workflow_1.runInitialSetupWorkflow)((0, initial_setup_request_1.createInitialSetupRequest)(param), { authenticatedUserPort: this.authenticatedUserPort, initialLabelProvisioningPort: this.initialLabelProvisioningPort, issueTypeProvisioningPort: this.issueTypeProvisioningPort, @@ -58089,46 +58808,45 @@ const result_1 = __nccwpck_require__(73817); const version_policy_1 = __nccwpck_require__(8381); const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); -const setup_configuration_policy_1 = __nccwpck_require__(56637); +const setup_resource_provisioning_1 = __nccwpck_require__(94894); const TASK_ID = 'InitialSetupUseCase'; /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ -async function runInitialSetupWorkflow(param, dependencies) { +async function runInitialSetupWorkflow(request, dependencies) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(TASK_ID)} Executing ${TASK_ID}.`); const steps = []; const errors = []; try { - const setupConfiguration = getSetupConfiguration(param); - if (!dependencies.setupWorkspacePort.hasValidToken(param.tokens.token)) { + const setupConfiguration = request.setupConfiguration; + if (!dependencies.setupWorkspacePort.hasValidToken(request.token)) { (0, logging_ports_1.logInfo)(' 🛑 Setup requires the setup PAT provided for this command with a valid token.'); errors.push('A valid setup PAT must be provided to run setup. It is separate from the workflow PAT Secret.'); return [buildResult(errors, steps)]; } (0, logging_ports_1.logInfo)('📋 Ensuring .github and copying setup files...'); - const workflowUpdates = getWorkflowUpdates(param); const workspaceSelection = { features: setupConfiguration?.features, - ...(workflowUpdates.length > 0 ? { + ...(request.workflowUpdates.length > 0 ? { updateExistingWorkflows: true, - approvedWorkflowFiles: workflowUpdates, + approvedWorkflowFiles: request.workflowUpdates, } : {}), }; const filesResult = dependencies.setupWorkspacePort.prepare(workspaceSelection); steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); (0, logging_ports_1.logInfo)('🔐 Checking GitHub access...'); - const githubAccess = await verifyGitHubAccess(param, dependencies.authenticatedUserPort); + const githubAccess = await verifyGitHubAccess(request, dependencies.authenticatedUserPort); if (!githubAccess.success) { errors.push(...githubAccess.errors); return [buildResult(errors, steps)]; } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); - const remoteConfiguration = await resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors); - const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration); + const remoteConfiguration = await (0, setup_resource_provisioning_1.resolveRemoteConfiguration)(request, dependencies, setupConfiguration, errors); + const secrets = await (0, setup_resource_provisioning_1.ensureRepositorySecrets)(request, dependencies, setupConfiguration, remoteConfiguration); if (secrets.step) steps.push(secrets.step); if (secrets.errors.length > 0) errors.push(...secrets.errors); (0, logging_ports_1.logInfo)('🏷️ Checking configured and progress labels...'); - const labels = await ensureInitialLabels(param, dependencies.initialLabelProvisioningPort); + const labels = await ensureInitialLabels(request, dependencies.initialLabelProvisioningPort); if (!labels.completed) { errors.push(labels.error); } @@ -58137,19 +58855,19 @@ async function runInitialSetupWorkflow(param, dependencies) { appendLabelSummary(steps, errors, labels.progress, 'Progress labels'); } (0, logging_ports_1.logInfo)('📋 Checking issue types...'); - const issueTypes = await ensureIssueTypes(param, dependencies.issueTypeProvisioningPort); + const issueTypes = await ensureIssueTypes(request, dependencies.issueTypeProvisioningPort); if (!issueTypes.success) { errors.push(...issueTypes.errors); } else { steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration); + const variables = await (0, setup_resource_provisioning_1.ensureRepositoryVariables)(request, dependencies, setupConfiguration, remoteConfiguration); if (variables.step) steps.push(variables.step); if (variables.errors.length > 0) errors.push(...variables.errors); - const defaultVersion = await ensureDefaultVersion(param, dependencies, setupConfiguration); + const defaultVersion = await ensureDefaultVersion(request, dependencies, setupConfiguration); if (defaultVersion.step) steps.push(defaultVersion.step); if (defaultVersion.error) @@ -58162,9 +58880,9 @@ async function runInitialSetupWorkflow(param, dependencies) { return [buildResult(errors, steps)]; } } -async function verifyGitHubAccess(param, repository) { +async function verifyGitHubAccess(request, repository) { try { - const user = await repository.getUserFromToken(param.tokens.token); + const user = await repository.getUserFromToken(request.token); return { success: true, user, errors: [] }; } catch (error) { @@ -58172,9 +58890,9 @@ async function verifyGitHubAccess(param, repository) { return { success: false, errors: [`Could not verify GitHub access: ${error}`] }; } } -async function ensureInitialLabels(param, repository) { +async function ensureInitialLabels(request, repository) { try { - const summary = await repository.ensureInitialLabels(param.owner, param.repo, param.labels, param.tokens.token); + const summary = await repository.ensureInitialLabels(request.owner, request.repo, request.labels, request.token); return { completed: true, ...summary }; } catch (error) { @@ -58183,9 +58901,9 @@ async function ensureInitialLabels(param, repository) { return { completed: false, error: message }; } } -async function ensureIssueTypes(param, repository) { +async function ensureIssueTypes(request, repository) { try { - const result = await repository.ensureIssueTypes(param.owner, param.issueTypes, param.tokens.token); + const result = await repository.ensureIssueTypes(request.owner, request.issueTypes, request.token); return { success: result.errors.length === 0, created: result.created, @@ -58198,7 +58916,7 @@ async function ensureIssueTypes(param, repository) { return { success: false, created: 0, existing: 0, errors: [`Error ensuring issue types: ${error}`] }; } } -async function ensureDefaultVersion(param, dependencies, setupConfiguration) { +async function ensureDefaultVersion(request, dependencies, setupConfiguration) { if (setupConfiguration?.createInitialTag === false) { return { step: '⏭️ Initial version tag creation disabled by setup configuration.' }; } @@ -58209,16 +58927,16 @@ async function ensureDefaultVersion(param, dependencies, setupConfiguration) { return {}; } (0, logging_ports_1.logInfo)(`🏷️ No version tags found. Creating default tag ${version_policy_1.DEFAULT_INITIAL_TAG}...`); - const defaultBranch = await dependencies.repositoryDefaultBranchPort.getDefaultBranch(param.owner, param.repo, param.tokens.token); + const defaultBranch = await dependencies.repositoryDefaultBranchPort.getDefaultBranch(request.owner, request.repo, request.token); if (!defaultBranch) { const message = 'Could not get default branch to create initial version tag.'; (0, logging_ports_1.logError)(message); return { error: message }; } - const sha = await dependencies.repositoryTagPort.createTag(param.owner, param.repo, defaultBranch, version_policy_1.DEFAULT_INITIAL_TAG, param.tokens.token); + const sha = await dependencies.repositoryTagPort.createTag(request.owner, request.repo, defaultBranch, version_policy_1.DEFAULT_INITIAL_TAG, request.token); return sha ? { step: `✅ Default version tag ${version_policy_1.DEFAULT_INITIAL_TAG} created on branch ${defaultBranch}. Run \`git fetch --tags\` to update local refs.` } - : { error: `Failed to create tag ${version_policy_1.DEFAULT_INITIAL_TAG} on ${param.owner}/${param.repo}` }; + : { error: `Failed to create tag ${version_policy_1.DEFAULT_INITIAL_TAG} on ${request.owner}/${request.repo}` }; } catch (error) { const message = `Error ensuring default version: ${error}`; @@ -58226,144 +58944,6 @@ async function ensureDefaultVersion(param, dependencies, setupConfiguration) { return { error: message }; } } -function getSetupConfiguration(param) { - const configuration = param.inputs?.setupConfiguration; - return configuration && typeof configuration === 'object' - ? configuration - : undefined; -} -function getWorkflowUpdates(param) { - const updates = param.inputs?.setupWorkflowUpdates; - return Array.isArray(updates) ? updates.filter((file) => typeof file === 'string') : []; -} -async function ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration) { - if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { - return { errors: [] }; - } - try { - const desired = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration); - const groups = groupResources(desired, 'variable', setupConfiguration, remoteConfiguration); - const result = await upsertVariableGroups(param, dependencies.setupRepositoryVariablesPort, groups); - if (result.errors.length > 0) - return { errors: result.errors }; - return { - step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, - errors: [], - }; - } - catch (error) { - const message = `Error configuring repository Variables: ${error}`; - (0, logging_ports_1.logError)(message); - return { errors: [message] }; - } -} -async function ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration) { - if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { - return { errors: [] }; - } - const credentials = getSetupCredentialCollection(param); - if (!credentials) { - return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; - } - const values = [ - ...(credentials.workflowPat ? [credentials.workflowPat] : []), - ...credentials.apiKeys, - ]; - if (values.length === 0) - return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; - try { - const groups = groupResources(values, 'secret', setupConfiguration, remoteConfiguration); - const result = await upsertSecretGroups(param, dependencies.setupRepositorySecretsPort, groups); - if (result.errors.length > 0) - return { errors: result.errors }; - return { - step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, - errors: [], - }; - } - catch (error) { - const message = `Error configuring repository Secrets: ${error}`; - (0, logging_ports_1.logError)(message); - return { errors: [message] }; - } -} -async function resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors) { - const provided = param.inputs?.setupRemoteConfiguration; - if (provided && typeof provided === 'object') - return provided; - if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) - return undefined; - try { - return await dependencies.setupRemoteConfigurationReadPort.inspect(param.owner, param.repo, param.tokens.token); - } - catch (error) { - const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; - (0, logging_ports_1.logError)(message); - if ((0, setup_configuration_policy_1.usesOrganizationStorage)(setupConfiguration)) - errors.push(message); - return undefined; - } -} -function groupResources(resources, kind, configuration, remoteConfiguration) { - const groups = new Map(); - for (const resource of resources) { - // Secret values reach this workflow only after the user chose keep/replace. - // Variables, however, are always generated from the selected setup contract, - // so preserveExisting must be applied here to avoid shadowing inherited values. - if (kind === 'variable' && !(0, setup_configuration_policy_1.shouldUpsertSetupResource)(configuration, kind, resource.name, remoteConfiguration)) - continue; - const target = (0, setup_configuration_policy_1.resolveSetupResourceTarget)(configuration, kind, resource.name, remoteConfiguration); - const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; - const group = groups.get(key) ?? { target, resources: [] }; - group.resources.push(resource); - groups.set(key, group); - } - return [...groups.values()]; -} -async function upsertVariableGroups(param, port, groups) { - let created = 0; - let updated = 0; - const errors = []; - for (const group of groups) { - if (group.target.scope === 'organization' && !port.upsertScopedVariables) { - errors.push('Organization Variable provisioning is not available in this installation.'); - continue; - } - const result = group.target.scope === 'organization' - ? await port.upsertScopedVariables(param.owner, param.repo, param.tokens.token, group.target, group.resources) - : await port.upsert(param.owner, param.repo, param.tokens.token, group.resources); - created += result.created; - updated += result.updated; - errors.push(...result.errors); - } - return { created, updated, errors }; -} -async function upsertSecretGroups(param, port, groups) { - let created = 0; - let updated = 0; - let skipped = 0; - const errors = []; - for (const group of groups) { - if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { - errors.push('Organization Secret provisioning is not available in this installation.'); - continue; - } - const result = group.target.scope === 'organization' - ? await port.upsertScopedSecrets(param.owner, param.repo, param.tokens.token, group.target, group.resources) - : await port.upsertSecrets(param.owner, param.repo, param.tokens.token, group.resources); - created += result.created; - updated += result.updated; - skipped += result.skipped; - errors.push(...result.errors); - } - return { created, updated, skipped, errors }; -} -function getSetupCredentialCollection(param) { - const credentials = param.inputs?.setupCredentials; - if (!credentials || typeof credentials !== 'object') - return undefined; - return credentials; -} function appendLabelSummary(steps, errors, summary, labelType) { if (summary.errors.length > 0) { errors.push(...summary.errors); @@ -58606,7 +59186,7 @@ exports.PublishGithubActionUseCase = PublishGithubActionUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runPublishGithubAction = runPublishGithubAction; const result_1 = __nccwpck_require__(73817); -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); async function runPublishGithubAction(param, taskId, repositoryTagPort, repositoryReleasePort) { const validationFailure = validateVersion(param, taskId); @@ -58634,7 +59214,7 @@ function validateVersion(param, taskId) { if (param.singleAction.version.length > 0) return undefined; (0, logging_ports_1.logError)('Version is not set.'); - return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); } function successResult(taskId, sourceTag, targetTag, releaseId) { (0, logging_ports_1.logInfo)(`Updated release \`${targetTag}\` from \`${sourceTag}\`: ${releaseId}`); @@ -58812,6 +59392,144 @@ function failure(taskId, message) { } +/***/ }), + +/***/ 94894: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ensureRepositoryVariables = ensureRepositoryVariables; +exports.ensureRepositorySecrets = ensureRepositorySecrets; +exports.resolveRemoteConfiguration = resolveRemoteConfiguration; +exports.groupSetupResources = groupSetupResources; +const setup_configuration_policy_1 = __nccwpck_require__(56637); +const logging_ports_1 = __nccwpck_require__(6152); +async function ensureRepositoryVariables(context, dependencies, setupConfiguration, remoteConfiguration) { + if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { + return { errors: [] }; + } + try { + const desired = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration); + const groups = groupSetupResources(desired, 'variable', setupConfiguration, remoteConfiguration); + const result = await upsertVariableGroups(context, dependencies.setupRepositoryVariablesPort, groups); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Variables: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +async function ensureRepositorySecrets(context, dependencies, setupConfiguration, remoteConfiguration) { + if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { + return { errors: [] }; + } + const credentials = context.setupCredentials; + if (!credentials) { + return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; + } + const values = [ + ...(credentials.workflowPat ? [credentials.workflowPat] : []), + ...credentials.apiKeys, + ]; + if (values.length === 0) + return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; + try { + const groups = groupSetupResources(values, 'secret', setupConfiguration, remoteConfiguration); + const result = await upsertSecretGroups(context, dependencies.setupRepositorySecretsPort, groups); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Secrets: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +async function resolveRemoteConfiguration(context, dependencies, setupConfiguration, errors) { + if (context.setupRemoteConfiguration) + return context.setupRemoteConfiguration; + if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) + return undefined; + try { + return await dependencies.setupRemoteConfigurationReadPort.inspect(context.owner, context.repo, context.token); + } + catch (error) { + const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + (0, logging_ports_1.logError)(message); + if ((0, setup_configuration_policy_1.usesOrganizationStorage)(setupConfiguration)) + errors.push(message); + return undefined; + } +} +/** Groups resources by their resolved storage target so each provider call is scoped explicitly. */ +function groupSetupResources(resources, kind, configuration, remoteConfiguration) { + const groups = new Map(); + for (const resource of resources) { + // Secret values reach this workflow only after the user chose keep/replace. + // Variables are generated from the selected setup contract, so preserving + // an inherited value must happen before the provider call is assembled. + if (kind === 'variable' && !(0, setup_configuration_policy_1.shouldUpsertSetupResource)(configuration, kind, resource.name, remoteConfiguration)) + continue; + const target = (0, setup_configuration_policy_1.resolveSetupResourceTarget)(configuration, kind, resource.name, remoteConfiguration); + const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; + const group = groups.get(key) ?? { target, resources: [] }; + group.resources.push(resource); + groups.set(key, group); + } + return [...groups.values()]; +} +async function upsertVariableGroups(context, port, groups) { + let created = 0; + let updated = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedVariables) { + errors.push('Organization Variable provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedVariables(context.owner, context.repo, context.token, group.target, group.resources) + : await port.upsert(context.owner, context.repo, context.token, group.resources); + created += result.created; + updated += result.updated; + errors.push(...result.errors); + } + return { created, updated, errors }; +} +async function upsertSecretGroups(context, port, groups) { + let created = 0; + let updated = 0; + let skipped = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { + errors.push('Organization Secret provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedSecrets(context.owner, context.repo, context.token, group.target, group.resources) + : await port.upsertSecrets(context.owner, context.repo, context.token, group.resources); + created += result.created; + updated += result.updated; + skipped += result.skipped; + errors.push(...result.errors); + } + return { created, updated, skipped, errors }; +} + + /***/ }), /***/ 18277: @@ -59421,7 +60139,7 @@ exports.ExecutionBranchVersionResolver = ExecutionBranchVersionResolver; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveEventIssueNumber = resolveEventIssueNumber; exports.resolveSingleActionIssueNumber = resolveSingleActionIssueNumber; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const positive_integer_policy_1 = __nccwpck_require__(19879); const title_utils_1 = __nccwpck_require__(46267); function resolveEventIssueNumber(execution) { @@ -59439,7 +60157,7 @@ function resolveEventIssueNumber(execution) { return positiveIssueNumberOrUndefined(execution.issueNumber); } async function resolveSingleActionIssueNumber(execution, issueRepository) { - const configuredIssue = execution.inputs?.[constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]; + const configuredIssue = execution.inputs?.[input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]; if (configuredIssue !== undefined && configuredIssue !== null && String(configuredIssue).trim() !== '') { const issueNumber = (0, positive_integer_policy_1.parsePositiveSafeInteger)(configuredIssue); return issueNumber === undefined ? undefined : setIssueNumber(execution, issueNumber); @@ -60280,7 +60998,7 @@ const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); const single_action_workflow_1 = __nccwpck_require__(6130); class SingleActionUseCase { - constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase) { + constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase, closeInactiveIssuesUseCase) { this.deployedActionUseCase = deployedActionUseCase; this.publishGithubActionUseCase = publishGithubActionUseCase; this.createReleaseUseCase = createReleaseUseCase; @@ -60290,6 +61008,7 @@ class SingleActionUseCase { this.checkProgressUseCase = checkProgressUseCase; this.detectPotentialProblemsUseCase = detectPotentialProblemsUseCase; this.recommendStepsUseCase = recommendStepsUseCase; + this.closeInactiveIssuesUseCase = closeInactiveIssuesUseCase; this.taskId = "SingleActionUseCase"; } async invoke(param) { @@ -60308,6 +61027,7 @@ class SingleActionUseCase { checkProgressUseCase: this.checkProgressUseCase, detectPotentialProblemsUseCase: this.detectPotentialProblemsUseCase, recommendStepsUseCase: this.recommendStepsUseCase, + closeInactiveIssuesUseCase: this.closeInactiveIssuesUseCase, }); } } @@ -60341,8 +61061,9 @@ async function runSingleActionWorkflow(param, taskId, ports) { { active: param.singleAction.isCheckProgressAction, useCase: ports.checkProgressUseCase }, { active: param.singleAction.isDetectPotentialProblemsAction, useCase: ports.detectPotentialProblemsUseCase }, { active: param.singleAction.isRecommendStepsAction, useCase: ports.recommendStepsUseCase }, - ].find(({ active }) => active); - if (!action) + { active: param.singleAction.isCloseInactiveIssuesAction, useCase: ports.closeInactiveIssuesUseCase }, + ].find(({ active, useCase }) => active && useCase !== undefined); + if (!action || !action.useCase) return []; try { return await action.useCase.invoke(param); @@ -60375,10 +61096,10 @@ exports.applyDetectedFindings = applyDetectedFindings; const prepare_bugbot_findings_1 = __nccwpck_require__(85016); const mark_findings_resolved_use_case_1 = __nccwpck_require__(96963); const publish_findings_use_case_1 = __nccwpck_require__(88442); -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); const pull_request_review_errors_1 = __nccwpck_require__(46445); function prepareDetectedFindings(execution, response) { - return (0, prepare_bugbot_findings_1.prepareBugbotFindings)(response, execution.ai?.getAiIgnoreFiles?.() ?? [], execution.ai?.getBugbotMinSeverity?.(), execution.ai?.getBugbotCommentLimit?.() ?? constants_1.BUGBOT_MAX_COMMENTS); + return (0, prepare_bugbot_findings_1.prepareBugbotFindings)(response, execution.ai?.getAiIgnoreFiles?.() ?? [], execution.ai?.getBugbotMinSeverity?.(), execution.ai?.getBugbotCommentLimit?.() ?? bugbot_constants_1.BUGBOT_MAX_COMMENTS); } async function applyDetectedFindings(execution, context, prepared, publicationPorts, resolutionPorts) { try { @@ -61714,12 +62435,12 @@ async function restoreStashedChanges(gitCommitPort) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.applyCommentLimit = applyCommentLimit; -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); /** * Applies the max-comments limit: returns the first N findings to publish individually, * and overflow count + titles for a single "revisar en local" summary comment. */ -function applyCommentLimit(findings, maxComments = constants_1.BUGBOT_MAX_COMMENTS) { +function applyCommentLimit(findings, maxComments = bugbot_constants_1.BUGBOT_MAX_COMMENTS) { if (findings.length <= maxComments) { return { toPublish: findings, overflowCount: 0, overflowTitles: [] }; } @@ -61934,7 +62655,7 @@ exports.markerRegexForFinding = markerRegexForFinding; exports.replaceMarkerInBody = replaceMarkerInBody; exports.extractTitleFromBody = extractTitleFromBody; exports.buildCommentBody = buildCommentBody; -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); const application_error_1 = __nccwpck_require__(75999); const github_comment_publication_policy_1 = __nccwpck_require__(72712); /** Maximum lossless finding identity accepted by the marker contract. */ @@ -61973,13 +62694,13 @@ function buildMarker(findingId, resolved, fingerprint, resolution) { const safeResolution = resolved && resolution && ['fixed', 'obsolete', 'dismissed'].includes(resolution) ? ` finding_resolution:"${resolution}"` : ''; - return ``; + return ``; } function parseMarker(body) { if (!body) return []; const results = []; - const regex = new RegExp(``, "g"); + const regex = new RegExp(``, "g"); let m; while ((m = regex.exec(body)) !== null) { results.push({ @@ -62000,7 +62721,7 @@ function markerRegexForFinding(findingId) { const idForRegex = SAFE_FINDING_ID_REGEX_CHARS.test(safeId) ? safeId : safeId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return new RegExp(``, "g"); + return new RegExp(``, "g"); } /** * Find the marker for this finding in body (using same pattern as parseMarker) and replace it. @@ -66339,6 +67060,20 @@ function queueTimeoutError() { } +/***/ }), + +/***/ 81853: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ERRORS = void 0; +exports.ERRORS = { + GIT_REPOSITORY_NOT_FOUND: '❌ Git repository not found', +}; + + /***/ }), /***/ 40149: @@ -66525,7 +67260,7 @@ function registerCliCommands(program) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.registerCheckProgressCommand = registerCheckProgressCommand; const local_action_1 = __nccwpck_require__(76102); -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); const cli_context_1 = __nccwpck_require__(21307); const command_input_policy_1 = __nccwpck_require__(95212); @@ -66533,7 +67268,7 @@ const issue_command_policy_1 = __nccwpck_require__(66915); function registerCheckProgressCommand(program) { program .command('check-progress') - .description(`${constants_1.TITLE} - Check progress of an issue based on code changes`) + .description(`${product_identity_1.TITLE} - Check progress of an issue based on code changes`) .option('-i, --issue ', 'Issue number to check progress for (required)', '') .option('-b, --branch ', 'Branch name (optional, will try to determine from issue)') .option('-d, --debug', 'Debug mode', false) @@ -66581,7 +67316,7 @@ function registerCheckProgressCommand(program) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.registerDetectPotentialProblemsCommand = registerDetectPotentialProblemsCommand; const local_action_1 = __nccwpck_require__(76102); -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); const cli_context_1 = __nccwpck_require__(21307); const command_input_policy_1 = __nccwpck_require__(95212); @@ -66589,7 +67324,7 @@ const detect_potential_problems_policy_1 = __nccwpck_require__(87980); function registerDetectPotentialProblemsCommand(program) { program .command('detect-potential-problems') - .description(`${constants_1.TITLE} - Detect potential problems in the branch (bugbot): report as comments on issue and PR`) + .description(`${product_identity_1.TITLE} - Detect potential problems in the branch (bugbot): report as comments on issue and PR`) .option('-i, --issue ', 'Issue number (required)', '') .option('-b, --branch ', 'Branch name (optional, defaults to current git branch)', '') .option('-d, --debug', 'Debug mode', false) @@ -66633,7 +67368,8 @@ function registerDetectPotentialProblemsCommand(program) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildDetectPotentialProblemsParams = buildDetectPotentialProblemsParams; exports.resolveDetectIssueNumber = resolveDetectIssueNumber; -const constants_1 = __nccwpck_require__(15415); +const action_types_1 = __nccwpck_require__(19625); +const input_keys_1 = __nccwpck_require__(88539); const command_input_policy_1 = __nccwpck_require__(95212); function buildDetectPotentialProblemsParams(options, gitInfo, currentBranch) { if ('error' in gitInfo) @@ -66643,15 +67379,15 @@ function buildDetectPotentialProblemsParams(options, gitInfo, currentBranch) { return undefined; const branch = ((0, command_input_policy_1.cleanCliArgument)(options.branch) || currentBranch).trim() || 'main'; return { - [constants_1.INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', - [constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.DETECT_POTENTIAL_PROBLEMS, - [constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: issueNumber, - [constants_1.INPUT_KEYS.TOKEN]: options.token || process.env.PERSONAL_ACCESS_TOKEN, + [input_keys_1.INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', + [input_keys_1.INPUT_KEYS.SINGLE_ACTION]: action_types_1.ACTIONS.DETECT_POTENTIAL_PROBLEMS, + [input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: issueNumber, + [input_keys_1.INPUT_KEYS.TOKEN]: options.token || process.env.PERSONAL_ACCESS_TOKEN, repo: { owner: gitInfo.owner, repo: gitInfo.repo }, issue: { number: issueNumber }, commits: { ref: `refs/heads/${branch}` }, - [constants_1.INPUT_KEYS.WELCOME_TITLE]: '🐛 Detect potential problems (bugbot)', - [constants_1.INPUT_KEYS.WELCOME_MESSAGES]: [`Detecting potential problems for issue #${issueNumber} on branch ${branch} in ${gitInfo.owner}/${gitInfo.repo}...`], + [input_keys_1.INPUT_KEYS.WELCOME_TITLE]: '🐛 Detect potential problems (bugbot)', + [input_keys_1.INPUT_KEYS.WELCOME_MESSAGES]: [`Detecting potential problems for issue #${issueNumber} on branch ${branch} in ${gitInfo.owner}/${gitInfo.repo}...`], }; } function resolveDetectIssueNumber(value) { @@ -66668,12 +67404,12 @@ function resolveDetectIssueNumber(value) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.registerDoCommand = registerDoCommand; -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const do_command_handler_1 = __nccwpck_require__(85235); function registerDoCommand(program) { program .command('do') - .description(`${constants_1.TITLE} - AI development assistant (selected build agent; can edit files when run locally)`) + .description(`${product_identity_1.TITLE} - AI development assistant (selected build agent; can edit files when run locally)`) .option('-p, --prompt ', 'Prompt or question (required)', '') .option('-d, --debug', 'Debug mode', false) .option('--agent-provider ', 'Agent provider (codex|opencode|cursor)') @@ -66955,12 +67691,13 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.parseIssueNumber = parseIssueNumber; exports.buildCheckProgressParams = buildCheckProgressParams; exports.buildRecommendStepsParams = buildRecommendStepsParams; -const constants_1 = __nccwpck_require__(15415); +const action_types_1 = __nccwpck_require__(19625); +const input_keys_1 = __nccwpck_require__(88539); const command_input_policy_1 = __nccwpck_require__(95212); function sharedOptions(options) { return { - [constants_1.INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', - [constants_1.INPUT_KEYS.TOKEN]: options.token || process.env.PERSONAL_ACCESS_TOKEN, + [input_keys_1.INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', + [input_keys_1.INPUT_KEYS.TOKEN]: options.token || process.env.PERSONAL_ACCESS_TOKEN, }; } function parseIssueNumber(value) { @@ -66975,14 +67712,14 @@ function buildCheckProgressParams(options, gitInfo) { const branch = (0, command_input_policy_1.cleanCliArgument)(options.branch); return { ...sharedOptions(options), - [constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.CHECK_PROGRESS, - [constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: issueNumber, - [constants_1.INPUT_KEYS.AI_IGNORE_FILES]: process.env.AI_IGNORE_FILES || 'build/*,dist/*,node_modules/*,*.d.ts', + [input_keys_1.INPUT_KEYS.SINGLE_ACTION]: action_types_1.ACTIONS.CHECK_PROGRESS, + [input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: issueNumber, + [input_keys_1.INPUT_KEYS.AI_IGNORE_FILES]: process.env.AI_IGNORE_FILES || 'build/*,dist/*,node_modules/*,*.d.ts', repo: { owner: gitInfo.owner, repo: gitInfo.repo }, issue: { number: issueNumber }, ...(branch ? { commits: { ref: `refs/heads/${branch}` } } : {}), - [constants_1.INPUT_KEYS.WELCOME_TITLE]: '📊 Progress Check', - [constants_1.INPUT_KEYS.WELCOME_MESSAGES]: [`Checking progress for issue #${issueNumber} in ${gitInfo.owner}/${gitInfo.repo}...`], + [input_keys_1.INPUT_KEYS.WELCOME_TITLE]: '📊 Progress Check', + [input_keys_1.INPUT_KEYS.WELCOME_MESSAGES]: [`Checking progress for issue #${issueNumber} in ${gitInfo.owner}/${gitInfo.repo}...`], }; } function buildRecommendStepsParams(options, gitInfo) { @@ -66993,12 +67730,12 @@ function buildRecommendStepsParams(options, gitInfo) { return undefined; return { ...sharedOptions(options), - [constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.RECOMMEND_STEPS, - [constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: issueNumber, + [input_keys_1.INPUT_KEYS.SINGLE_ACTION]: action_types_1.ACTIONS.RECOMMEND_STEPS, + [input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: issueNumber, repo: { owner: gitInfo.owner, repo: gitInfo.repo }, issue: { number: issueNumber }, - [constants_1.INPUT_KEYS.WELCOME_TITLE]: '📋 Recommend steps', - [constants_1.INPUT_KEYS.WELCOME_MESSAGES]: [`Recommending steps for issue #${issueNumber} in ${gitInfo.owner}/${gitInfo.repo}...`], + [input_keys_1.INPUT_KEYS.WELCOME_TITLE]: '📋 Recommend steps', + [input_keys_1.INPUT_KEYS.WELCOME_MESSAGES]: [`Recommending steps for issue #${issueNumber} in ${gitInfo.owner}/${gitInfo.repo}...`], }; } @@ -67013,7 +67750,7 @@ function buildRecommendStepsParams(options, gitInfo) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.registerRecommendStepsCommand = registerRecommendStepsCommand; const local_action_1 = __nccwpck_require__(76102); -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); const cli_context_1 = __nccwpck_require__(21307); const command_input_policy_1 = __nccwpck_require__(95212); @@ -67021,7 +67758,7 @@ const issue_command_policy_1 = __nccwpck_require__(66915); function registerRecommendStepsCommand(program) { program .command('recommend-steps') - .description(`${constants_1.TITLE} - Recommend steps to implement an issue (configured agent)`) + .description(`${product_identity_1.TITLE} - Recommend steps to implement an issue (configured agent)`) .option('-i, --issue ', 'Issue number (required)', '') .option('-d, --debug', 'Debug mode', false) .option('-t, --token ', 'Personal access token (or PERSONAL_ACCESS_TOKEN from the environment)') @@ -67167,7 +67904,7 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.registerSetupCommand = registerSetupCommand; const local_action_1 = __nccwpck_require__(76102); -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const setup_files_1 = __nccwpck_require__(59126); const logger_1 = __nccwpck_require__(91151); const cli_context_1 = __nccwpck_require__(21307); @@ -67180,7 +67917,7 @@ const setup_workspace_adapter_1 = __nccwpck_require__(5729); function registerSetupCommand(program) { program .command('setup') - .description(`${constants_1.TITLE} - Interactive repository setup: select workflows, agents, Variables, labels, and issue types`) + .description(`${product_identity_1.TITLE} - Interactive repository setup: select workflows, agents, Variables, labels, and issue types`) .option('-d, --debug', 'Debug mode', false) .option('-t, --token ', 'Personal access token (or PERSONAL_ACCESS_TOKEN from the environment)') .option('--agent ', 'Use one agent runtime for every setup task (codex|opencode|cursor)') @@ -67388,21 +68125,22 @@ function parseVisibility(value, flag) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildSetupParams = buildSetupParams; -const constants_1 = __nccwpck_require__(15415); +const action_types_1 = __nccwpck_require__(19625); +const input_keys_1 = __nccwpck_require__(88539); const setup_configuration_policy_1 = __nccwpck_require__(56637); function buildSetupParams(options, gitInfo, token, configuration, credentials, approvedWorkflowFiles = [], remoteConfiguration) { if ('error' in gitInfo) return undefined; return { ...(configuration ? (0, setup_configuration_policy_1.buildSetupActionInputs)(configuration) : {}), - [constants_1.INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', - [constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.INITIAL_SETUP, - [constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: 1, - [constants_1.INPUT_KEYS.TOKEN]: token, + [input_keys_1.INPUT_KEYS.DEBUG]: options.debug?.toString() ?? 'false', + [input_keys_1.INPUT_KEYS.SINGLE_ACTION]: action_types_1.ACTIONS.INITIAL_SETUP, + [input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: 1, + [input_keys_1.INPUT_KEYS.TOKEN]: token, repo: { owner: gitInfo.owner, repo: gitInfo.repo }, issue: { number: 1 }, - [constants_1.INPUT_KEYS.WELCOME_TITLE]: '⚙️ Initial Setup', - [constants_1.INPUT_KEYS.WELCOME_MESSAGES]: [ + [input_keys_1.INPUT_KEYS.WELCOME_TITLE]: '⚙️ Initial Setup', + [input_keys_1.INPUT_KEYS.WELCOME_MESSAGES]: [ `Running initial setup for ${gitInfo.owner}/${gitInfo.repo}...`, 'This will install the selected workflows, configure repository Variables, create labels and issue types, and verify access to GitHub.', ], @@ -67423,12 +68161,12 @@ function buildSetupParams(options, gitInfo, token, configuration, credentials, a Object.defineProperty(exports, "__esModule", ({ value: true })); exports.registerThinkCommand = registerThinkCommand; -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const think_command_handler_1 = __nccwpck_require__(85340); function registerThinkCommand(program) { program .command("think") - .description(`${constants_1.TITLE} - Deep code analysis and change proposals using AI reasoning`) + .description(`${product_identity_1.TITLE} - Deep code analysis and change proposals using AI reasoning`) .option("-i, --issue ", "Issue number to process (optional)", "1") .option("-b, --branch ", "Branch name", "master") .option("-d, --debug", "Debug mode", false) @@ -67451,7 +68189,8 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runThinkCommand = runThinkCommand; const local_action_1 = __nccwpck_require__(76102); const issue_metadata_composition_root_1 = __nccwpck_require__(95228); -const constants_1 = __nccwpck_require__(15415); +const action_types_1 = __nccwpck_require__(19625); +const input_keys_1 = __nccwpck_require__(88539); const logger_1 = __nccwpck_require__(91151); const cli_context_1 = __nccwpck_require__(21307); const command_input_policy_1 = __nccwpck_require__(95212); @@ -67473,18 +68212,18 @@ async function runThinkCommand(options) { const issueNumber = (0, command_input_policy_1.cleanCliArgument)(options.issue) || "1"; const token = resolveOption(options.token, "PERSONAL_ACCESS_TOKEN"); const params = { - [constants_1.INPUT_KEYS.DEBUG]: String(options.debug ?? false), - [constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.THINK, - [constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: parseInt(issueNumber, 10) || 1, - [constants_1.INPUT_KEYS.TOKEN]: token, - [constants_1.INPUT_KEYS.AI_IGNORE_FILES]: resolveOption(options.aiIgnoreFiles, "AI_IGNORE_FILES"), - [constants_1.INPUT_KEYS.AI_INCLUDE_REASONING]: resolveOption(options.includeReasoning, "AI_INCLUDE_REASONING"), + [input_keys_1.INPUT_KEYS.DEBUG]: String(options.debug ?? false), + [input_keys_1.INPUT_KEYS.SINGLE_ACTION]: action_types_1.ACTIONS.THINK, + [input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: parseInt(issueNumber, 10) || 1, + [input_keys_1.INPUT_KEYS.TOKEN]: token, + [input_keys_1.INPUT_KEYS.AI_IGNORE_FILES]: resolveOption(options.aiIgnoreFiles, "AI_IGNORE_FILES"), + [input_keys_1.INPUT_KEYS.AI_INCLUDE_REASONING]: resolveOption(options.includeReasoning, "AI_INCLUDE_REASONING"), repo: { owner: gitInfo.owner, repo: gitInfo.repo }, commits: { ref: `refs/heads/${branch}` }, }; await addIssueContext(params, gitInfo.owner, gitInfo.repo, issueNumber, token, question); - params[constants_1.INPUT_KEYS.WELCOME_TITLE] = "🤔 AI Reasoning Analysis"; - params[constants_1.INPUT_KEYS.WELCOME_MESSAGES] = [ + params[input_keys_1.INPUT_KEYS.WELCOME_TITLE] = "🤔 AI Reasoning Analysis"; + params[input_keys_1.INPUT_KEYS.WELCOME_MESSAGES] = [ `Starting deep code analysis for ${gitInfo.owner}/${gitInfo.repo}/${branch}...`, `Question: ${question.substring(0, 100)}${question.length > 100 ? "..." : ""}`, ]; @@ -67614,7 +68353,7 @@ const REPOSITORY_STRING_KEYS = new Set([ 'commitPrefixTransforms', ]); const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush']); -const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout']); +const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout', 'inactivityThresholdHours']); const AI_BOOLEAN_KEYS = new Set(['pullRequestDescription', 'membersOnly', 'includeReasoning']); const AI_STRING_KEYS = new Set(['ignoreFiles', 'pullRequestDescriptionMode', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); const AI_NUMBER_KEYS = new Set(['bugbotCommentLimit']); @@ -67780,6 +68519,7 @@ exports.SetupPromptAdapter = void 0; const promises_1 = __nccwpck_require__(32887); const node_process_1 = __nccwpck_require__(97742); const setup_configuration_policy_1 = __nccwpck_require__(56637); +const setup_prompt_rendering_1 = __nccwpck_require__(83434); const AGENT_PROVIDERS = ['codex', 'opencode', 'cursor']; const MODEL_PROVIDERS = ['openai', 'anthropic', 'google', 'openrouter', 'opencode', 'local']; class SetupPromptAdapter { @@ -67795,14 +68535,14 @@ class SetupPromptAdapter { async collect(defaults) { if (!this.readline) return defaults; - console.log(renderBox('This wizard configures repository workflows, GitHub Variables, GitHub Secrets, AI agents, and operational defaults.\n\nThe setup PAT is an operator credential used only during this command. It is different from the workflow PAT that the bot account uses at runtime.', 'Copilot Setup')); - console.log(color('\n1. Choose the capabilities to install\n', 36)); + console.log((0, setup_prompt_rendering_1.renderBox)('This wizard configures repository workflows, GitHub Variables, GitHub Secrets, AI agents, and operational defaults.\n\nThe setup PAT is an operator credential used only during this command. It is different from the workflow PAT that the bot account uses at runtime.', 'Copilot Setup')); + console.log((0, setup_prompt_rendering_1.color)('\n1. Choose the capabilities to install\n', 36)); for (const [feature, description] of Object.entries(setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS)) { defaults.features[feature] = await this.askBoolean(description, defaults.features[feature] !== false); } - console.log(color('\n2. Choose one of the three supported agent runtimes for each task\n', 36)); + console.log((0, setup_prompt_rendering_1.color)('\n2. Choose one of the three supported agent runtimes for each task\n', 36)); for (const task of setup_configuration_policy_1.SETUP_AGENT_TASKS) { - defaults.agents[task].provider = await this.askChoice(`${formatTask(task)} runtime`, [...AGENT_PROVIDERS], defaults.agents[task].provider); + defaults.agents[task].provider = await this.askChoice(`${(0, setup_prompt_rendering_1.formatTask)(task)} runtime`, [...AGENT_PROVIDERS], defaults.agents[task].provider); } const modelProvider = await this.askChoice('Model provider for all tasks', [...MODEL_PROVIDERS], defaults.agents.findings.modelProvider); const model = await this.askText('Model name for all tasks', defaults.agents.findings.model); @@ -67814,12 +68554,12 @@ class SetupPromptAdapter { } if (await this.askBoolean('Configure model provider, model, and effort independently for every task?', false)) { for (const task of setup_configuration_policy_1.SETUP_AGENT_TASKS) { - defaults.agents[task].modelProvider = await this.askText(`${formatTask(task)} model provider`, defaults.agents[task].modelProvider); - defaults.agents[task].model = await this.askText(`${formatTask(task)} model`, defaults.agents[task].model); - defaults.agents[task].effort = await this.askText(`${formatTask(task)} effort (empty for default)`, defaults.agents[task].effort ?? ''); + defaults.agents[task].modelProvider = await this.askText(`${(0, setup_prompt_rendering_1.formatTask)(task)} model provider`, defaults.agents[task].modelProvider); + defaults.agents[task].model = await this.askText(`${(0, setup_prompt_rendering_1.formatTask)(task)} model`, defaults.agents[task].model); + defaults.agents[task].effort = await this.askText(`${(0, setup_prompt_rendering_1.formatTask)(task)} effort (empty for default)`, defaults.agents[task].effort ?? ''); } } - console.log(color('\n3. Configure repository behavior\n', 36)); + console.log((0, setup_prompt_rendering_1.color)('\n3. Configure repository behavior\n', 36)); const repository = defaults.repository; repository.mainBranch = await this.askText('Production branch', repository.mainBranch); repository.developmentBranch = await this.askText('Development branch', repository.developmentBranch); @@ -67834,10 +68574,11 @@ class SetupPromptAdapter { repository.desiredAssigneesCount = await this.askNumber('Desired issue assignees (0 disables automatic assignment)', repository.desiredAssigneesCount); repository.desiredReviewersCount = await this.askNumber('Desired pull-request reviewers (0 disables automatic assignment)', repository.desiredReviewersCount); repository.mergeTimeout = await this.askNumber('Merge timeout in seconds (0 disables the timeout)', repository.mergeTimeout); + repository.inactivityThresholdHours = await this.askNumber('Hours without activity before closing a waiting issue', repository.inactivityThresholdHours); repository.issueLocale = await this.askText('Issue comment locale', repository.issueLocale); repository.pullRequestLocale = await this.askText('Pull-request comment locale', repository.pullRequestLocale); repository.commitPrefixTransforms = await this.askText('Commit prefix transforms', repository.commitPrefixTransforms); - console.log(color('\n4. Configure AI, projects, and release safety\n', 36)); + console.log((0, setup_prompt_rendering_1.color)('\n4. Configure AI, projects, and release safety\n', 36)); const ai = defaults.ai; ai.pullRequestDescription = await this.askBoolean('Generate AI pull-request descriptions?', ai.pullRequestDescription); ai.pullRequestDescriptionMode = await this.askChoice('Pull-request description mode', ['replace', 'append', 'preserve', 'disabled'], ai.pullRequestDescriptionMode ?? 'replace'); @@ -67863,8 +68604,8 @@ class SetupPromptAdapter { async chooseStorage(defaults, remote, variables, requirements, managed = { secrets: true, variables: true }) { if (!this.readline) return defaults; - console.log(color('\n5. Review GitHub Actions resource scopes\n', 36)); - console.log(renderBox(renderRemoteConfiguration(remote, variables, requirements), 'Existing GitHub Actions resources', 33)); + console.log((0, setup_prompt_rendering_1.color)('\n5. Review GitHub Actions resource scopes\n', 36)); + console.log((0, setup_prompt_rendering_1.renderBox)((0, setup_prompt_rendering_1.renderRemoteConfiguration)(remote, variables, requirements), 'Existing GitHub Actions resources', 33)); const secrets = managed.secrets ? await this.chooseStoragePolicy('secrets', defaults.secrets, remote, requirements.map(requirement => requirement.name)) : defaults.secrets; @@ -67878,15 +68619,15 @@ class SetupPromptAdapter { showPlan(plan) { const enabledFeatures = Object.entries(plan.configuration.features) .filter(([, enabled]) => enabled) - .map(([feature]) => ` ${color('✓', 32)} ${setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS[feature] ?? feature}`) + .map(([feature]) => ` ${(0, setup_prompt_rendering_1.color)('✓', 32)} ${setup_configuration_policy_1.SETUP_FEATURE_DESCRIPTIONS[feature] ?? feature}`) .join('\n'); const agents = setup_configuration_policy_1.SETUP_AGENT_TASKS - .map(task => ` ${formatTask(task)}: ${plan.configuration.agents[task].provider} / ${plan.configuration.agents[task].modelProvider}/${plan.configuration.agents[task].model}`) + .map(task => ` ${(0, setup_prompt_rendering_1.formatTask)(task)}: ${plan.configuration.agents[task].provider} / ${plan.configuration.agents[task].modelProvider}/${plan.configuration.agents[task].model}`) .join('\n'); const content = [ - color('Capabilities', 36), enabledFeatures || ' (none)', '', - color('Agent routing', 36), agents, '', - color('Repository changes', 36), + (0, setup_prompt_rendering_1.color)('Capabilities', 36), enabledFeatures || ' (none)', '', + (0, setup_prompt_rendering_1.color)('Agent routing', 36), agents, '', + (0, setup_prompt_rendering_1.color)('Repository changes', 36), ` Files selected: ${plan.selectedFiles.length}`, ` Variables to upsert: ${plan.configuration.manageRepositoryVariables ? plan.variables.length : 0}`, ` Secrets to validate/provision: ${plan.configuration.manageRepositorySecrets ? plan.credentialRequirements.length : 0}`, @@ -67894,10 +68635,10 @@ class SetupPromptAdapter { ` Secret storage: ${plan.configuration.storage.secrets.defaultScope} scope${plan.configuration.storage.secrets.defaultScope === 'organization' ? ` (${plan.configuration.storage.secrets.organizationVisibility})` : ''}`, ` Labels and issue types: always checked by Copilot setup`, ` Initial tag: ${plan.configuration.createInitialTag ? 'v1.0.0 when no version tag exists' : 'disabled'}`, '', - color('Credential contract', 33), ` ${plan.requiredSecrets.join(', ')}`, - ...(plan.warnings.length > 0 ? ['', color('Important notes', 33), ...plan.warnings.map(warning => ` ⚠ ${warning}`)] : []), + (0, setup_prompt_rendering_1.color)('Credential contract', 33), ` ${plan.requiredSecrets.join(', ')}`, + ...(plan.warnings.length > 0 ? ['', (0, setup_prompt_rendering_1.color)('Important notes', 33), ...plan.warnings.map(warning => ` ⚠ ${warning}`)] : []), ].join('\n'); - console.log(renderBox(content, 'Setup Plan', 32)); + console.log((0, setup_prompt_rendering_1.renderBox)(content, 'Setup Plan', 32)); } async confirm(plan) { if (this.assumeYes || !this.readline) @@ -67907,13 +68648,13 @@ class SetupPromptAdapter { async requestSetupPat() { if (!this.readline) return undefined; - console.log(renderBox('Enter a GitHub setup PAT. It is used in memory for this run only and is never stored in the repository, a .env file, or a GitHub Secret.\n\nRecommended fine-grained permissions for the selected setup features:\n Repository: Metadata read, Contents read, Issues write, Actions read/write, Variables write, Secrets read/write, Workflows read/write.\n Organization: Issue Types write and Projects read/write only when selected; Members read when member-only checks are enabled.\n Contents write and Workflows write are needed only when changing workflow files through the GitHub API.\n\nThe workflow PAT is a different bot-account token and is requested separately.', 'Setup PAT', 33)); + console.log((0, setup_prompt_rendering_1.renderBox)('Enter a GitHub setup PAT. It is used in memory for this run only and is never stored in the repository, a .env file, or a GitHub Secret.\n\nRecommended fine-grained permissions for the selected setup features:\n Repository: Metadata read, Contents read, Issues write, Actions read/write, Variables write, Secrets read/write, Workflows read/write.\n Organization: Issue Types write and Projects read/write only when selected; Members read when member-only checks are enabled.\n Contents write and Workflows write are needed only when changing workflow files through the GitHub API.\n\nThe workflow PAT is a different bot-account token and is requested separately.', 'Setup PAT', 33)); return this.askSecret('Setup PAT'); } explainCredentialSeparation(requirements) { if (!this.readline) return; - console.log(renderBox('The workflow PAT is not the setup PAT. The workflow PAT belongs to the bot account, is stored remotely as the PAT Secret, and is used by GitHub Actions to work on issues and pull requests. Existing Secrets are never readable through GitHub; Copilot can only validate them through the repository health workflow.', 'Workflow credentials', 33)); + console.log((0, setup_prompt_rendering_1.renderBox)('The workflow PAT is not the setup PAT. The workflow PAT belongs to the bot account, is stored remotely as the PAT Secret, and is used by GitHub Actions to work on issues and pull requests. Existing Secrets are never readable through GitHub; Copilot can only validate them through the repository health workflow.', 'Workflow credentials', 33)); console.log(`Required credentials: ${requirements.map(requirement => requirement.name).join(', ')}`); } async requestWorkflowPat(requirement, current) { @@ -67933,11 +68674,11 @@ class SetupPromptAdapter { showCredentialChecks(checks) { if (checks.length === 0) return; - console.log(renderBox(checks.map(check => ` ${statusIcon(check.status)} ${check.name}: ${check.status} — ${check.message}`).join('\n'), 'Credential validation', checks.some(check => check.status === 'invalid') ? 31 : 32)); + console.log((0, setup_prompt_rendering_1.renderBox)(checks.map(check => ` ${(0, setup_prompt_rendering_1.statusIcon)(check.status)} ${check.name}: ${check.status} — ${check.message}`).join('\n'), 'Credential validation', checks.some(check => check.status === 'invalid') ? 31 : 32)); } showDoctorChecks(checks) { - const content = checks.map(check => ` ${doctorIcon(check.status)} ${check.area}: ${check.message}`).join('\n'); - console.log(renderBox(content || ' No checks were available.', 'Copilot Doctor', checks.some(check => check.status === 'fail') ? 31 : 32)); + const content = checks.map(check => ` ${(0, setup_prompt_rendering_1.doctorIcon)(check.status)} ${check.area}: ${check.message}`).join('\n'); + console.log((0, setup_prompt_rendering_1.renderBox)(content || ' No checks were available.', 'Copilot Doctor', checks.some(check => check.status === 'fail') ? 31 : 32)); } async confirmWorkflowUpdates(comparisons, forcedByFlag) { const changed = comparisons.filter(comparison => comparison.status === 'changed' || comparison.status === 'unmanaged'); @@ -67945,7 +68686,7 @@ class SetupPromptAdapter { return false; if (!this.readline) return forcedByFlag; - console.log(renderBox(changed.map(comparison => ` ${comparison.status === 'changed' ? '↻' : '⚠'} ${comparison.destination} (${comparison.status})`).join('\n'), 'Existing workflows detected', 33)); + console.log((0, setup_prompt_rendering_1.renderBox)(changed.map(comparison => ` ${comparison.status === 'changed' ? '↻' : '⚠'} ${comparison.destination} (${comparison.status})`).join('\n'), 'Existing workflows detected', 33)); if (forcedByFlag) { console.log('The --update-workflows flag was provided; these setup-managed workflows are eligible for update.'); return true; @@ -67956,7 +68697,7 @@ class SetupPromptAdapter { this.readline?.close(); } async askText(question, defaultValue) { - const answer = await this.readline.question(`${question} ${color(`[${defaultValue || 'none'}]`, 90)}: `); + const answer = await this.readline.question(`${question} ${(0, setup_prompt_rendering_1.color)(`[${defaultValue || 'none'}]`, 90)}: `); return answer.trim() || defaultValue; } async requestSecretForRequirement(requirement, current, label) { @@ -68015,11 +68756,11 @@ class SetupPromptAdapter { const parsed = Number(value); if (Number.isInteger(parsed) && parsed >= 0) return parsed; - console.log(color('Please enter a non-negative whole number.', 33)); + console.log((0, setup_prompt_rendering_1.color)('Please enter a non-negative whole number.', 33)); } } async askBoolean(question, defaultValue) { - const answer = await this.readline.question(`${question} ${color(`[${defaultValue ? 'Y' : 'N'}]`, 90)}: `); + const answer = await this.readline.question(`${question} ${(0, setup_prompt_rendering_1.color)(`[${defaultValue ? 'Y' : 'N'}]`, 90)}: `); const normalized = answer.trim().toLowerCase(); if (!normalized) return defaultValue; @@ -68027,15 +68768,15 @@ class SetupPromptAdapter { } async askChoice(question, choices, defaultValue) { console.log(question); - choices.forEach((choice, index) => console.log(` ${index + 1}) ${choice}${choice === defaultValue ? color(' (default)', 90) : ''}`)); + choices.forEach((choice, index) => console.log(` ${index + 1}) ${choice}${choice === defaultValue ? (0, setup_prompt_rendering_1.color)(' (default)', 90) : ''}`)); while (true) { - const answer = await this.readline.question(`Select 1-${choices.length} ${color(`[${choices.indexOf(defaultValue) + 1}]`, 90)}: `); + const answer = await this.readline.question(`Select 1-${choices.length} ${(0, setup_prompt_rendering_1.color)(`[${choices.indexOf(defaultValue) + 1}]`, 90)}: `); if (!answer.trim()) return defaultValue; const index = Number(answer) - 1; if (Number.isInteger(index) && choices[index]) return choices[index]; - console.log(color('Please select one of the listed options.', 33)); + console.log((0, setup_prompt_rendering_1.color)('Please select one of the listed options.', 33)); } } async chooseStoragePolicy(kind, defaults, remote, names) { @@ -68065,6 +68806,23 @@ class SetupPromptAdapter { } } exports.SetupPromptAdapter = SetupPromptAdapter; + + +/***/ }), + +/***/ 83434: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.statusIcon = statusIcon; +exports.doctorIcon = doctorIcon; +exports.formatTask = formatTask; +exports.color = color; +exports.renderBox = renderBox; +exports.renderRemoteConfiguration = renderRemoteConfiguration; +const node_process_1 = __nccwpck_require__(97742); function statusIcon(status) { if (status === 'valid') return '✓'; @@ -68132,7 +68890,7 @@ exports.getGitInfo = getGitInfo; exports.getCurrentBranch = getCurrentBranch; exports.isInsideGitRepo = isInsideGitRepo; const child_process_1 = __nccwpck_require__(32081); -const constants_1 = __nccwpck_require__(15415); +const cli_errors_1 = __nccwpck_require__(81853); function cleanCliArg(value) { if (value == null) return ''; @@ -68144,11 +68902,11 @@ function getGitInfo() { const remoteUrl = (0, child_process_1.execSync)('git config --get remote.origin.url').toString().trim(); const match = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/]+)(?:\.git)?$/); if (!match) - return { error: constants_1.ERRORS.GIT_REPOSITORY_NOT_FOUND }; + return { error: cli_errors_1.ERRORS.GIT_REPOSITORY_NOT_FOUND }; return { owner: match[1], repo: match[2].replace('.git', '') }; } catch { - return { error: constants_1.ERRORS.GIT_REPOSITORY_NOT_FOUND }; + return { error: cli_errors_1.ERRORS.GIT_REPOSITORY_NOT_FOUND }; } } function getCurrentBranch() { @@ -68190,6 +68948,7 @@ exports.ACTIONS = { CHECK_PROGRESS: 'check_progress_action', DETECT_POTENTIAL_PROBLEMS: 'detect_potential_problems_action', RECOMMEND_STEPS: 'recommend_steps_action', + CLOSE_INACTIVE_ISSUES: 'close_inactive_issues_action', }; @@ -68474,6 +69233,7 @@ const label_branch_policy_1 = __nccwpck_require__(53318); const commit_1 = __nccwpck_require__(57525); const config_1 = __nccwpck_require__(90450); const github_user_policy_1 = __nccwpck_require__(84403); +const issue_inactivity_1 = __nccwpck_require__(38572); class Execution { get eventName() { return this.inputs?.eventName ?? ''; @@ -68565,6 +69325,7 @@ class Execution { this.project = components.projects; this.workflows = components.workflows; this.tokenUser = components.tokenUser; + this.inactivityThresholdHours = components.inactivityThresholdHours ?? issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS; this.currentConfiguration = new config_1.Config({}); this.inputs = components.inputs; this.welcome = components.welcome; @@ -69449,6 +70210,9 @@ class SingleAction { get isRecommendStepsAction() { return this.currentSingleAction === action_types_1.ACTIONS.RECOMMEND_STEPS; } + get isCloseInactiveIssuesAction() { + return this.currentSingleAction === action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES; + } get enabledSingleAction() { return this.currentSingleAction.length > 0; } @@ -69474,6 +70238,7 @@ class SingleAction { action_types_1.ACTIONS.CHECK_PROGRESS, action_types_1.ACTIONS.DETECT_POTENTIAL_PROBLEMS, action_types_1.ACTIONS.RECOMMEND_STEPS, + action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, ]; /** * Actions that throw an error if the last step failed @@ -69483,6 +70248,7 @@ class SingleAction { action_types_1.ACTIONS.CREATE_RELEASE, action_types_1.ACTIONS.DEPLOYED, action_types_1.ACTIONS.CREATE_TAG, + action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, ]; /** * Actions that do not require an issue @@ -69490,6 +70256,7 @@ class SingleAction { this.actionsWithoutIssue = [ action_types_1.ACTIONS.THINK, action_types_1.ACTIONS.INITIAL_SETUP, + action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, ]; this.isIssue = false; this.isPullRequest = false; @@ -70453,7 +71220,7 @@ function extractReasoningFromParts(parts) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.AgentCapabilityAdapter = void 0; -const constants_1 = __nccwpck_require__(15415); +const agent_constants_1 = __nccwpck_require__(46927); const logger_1 = __nccwpck_require__(91151); const provider_cli_adapter_1 = __nccwpck_require__(18199); const agent_configuration_policy_1 = __nccwpck_require__(49616); @@ -70467,7 +71234,7 @@ class AgentCapabilityAdapter { const output = await this.cliAdapter.execute({ configuration: taskConfiguration, prompt: this.addEffortInstruction(request.prompt, taskConfiguration.effort), - timeoutMs: constants_1.AGENT_REQUEST_TIMEOUT_MS, + timeoutMs: agent_constants_1.AGENT_REQUEST_TIMEOUT_MS, }); return request.mapCliOutput(output); } @@ -70486,6 +71253,19 @@ class AgentCapabilityAdapter { exports.AgentCapabilityAdapter = AgentCapabilityAdapter; +/***/ }), + +/***/ 46927: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.AGENT_REQUEST_TIMEOUT_MS = void 0; +/** Maximum time allowed for one external agent CLI request. */ +exports.AGENT_REQUEST_TIMEOUT_MS = 900000; + + /***/ }), /***/ 27725: @@ -71477,13 +72257,74 @@ exports.IssueContentRepository = IssueContentRepository; /***/ }), -/***/ 59699: +/***/ 28868: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.IssueLabelProvisioningRepository = void 0; +exports.IssueInactivityRepository = void 0; +const github_pagination_policy_1 = __nccwpck_require__(44812); +/** Reads the provider's issue activity timestamp and waiting-state labels. */ +class IssueInactivityRepository { + constructor(githubClient) { + this.githubClient = githubClient; + this.listOpenIssuesByLabel = async (owner, repository, label, token) => { + const client = this.githubClient.getClient(token); + const issues = []; + for await (const response of client.paginate.iterator(client.rest.issues.listForRepo, { + owner, + repo: repository, + state: 'open', + labels: label, + sort: 'updated', + direction: 'asc', + per_page: 100, + })) { + const page = (0, github_pagination_policy_1.requireArrayPage)(response.data, 'open issues'); + issues.push(...page.map(toSnapshot)); + } + return issues; + }; + this.getOpenIssue = async (owner, repository, issueNumber, token) => { + const client = this.githubClient.getClient(token); + const response = await client.rest.issues.get({ + owner, + repo: repository, + issue_number: issueNumber, + }); + if (response.data.state !== 'open') + return undefined; + return toSnapshot(response.data); + }; + } +} +exports.IssueInactivityRepository = IssueInactivityRepository; +function toSnapshot(issue) { + if (!Number.isSafeInteger(issue.number) || issue.number < 1) { + throw new Error('GitHub issue response contained an invalid issue number.'); + } + return { + number: issue.number, + updatedAt: issue.updated_at ?? undefined, + isPullRequest: issue.pull_request !== undefined, + labels: (issue.labels ?? []).flatMap(label => { + const name = typeof label === 'string' ? label : label.name; + return name?.trim() ? [name] : []; + }), + }; +} + + +/***/ }), + +/***/ 59699: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.IssueLabelProvisioningRepository = void 0; const initial_label_provisioning_policy_1 = __nccwpck_require__(73160); const logger_1 = __nccwpck_require__(91151); const github_error_policy_1 = __nccwpck_require__(58791); @@ -74574,7 +75415,7 @@ function encryptSecret(value, base64PublicKey) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ActivePreviousWorkflowRunsRepository = void 0; -const constants_1 = __nccwpck_require__(15415); +const workflow_status_1 = __nccwpck_require__(1462); const workflow_runs_retry_1 = __nccwpck_require__(86434); const NO_OP_DELAY_PORT = { wait: async () => undefined }; const SYSTEM_CLOCK = { nowMilliseconds: () => Date.now() }; @@ -74614,7 +75455,7 @@ class ActivePreviousWorkflowRunsRepository { return (0, workflow_runs_retry_1.withWorkflowRunsRetry)(async () => { let activeRunCount = 0; // Keep one complete sequential traversal: GitHub cannot safely express - // the seven shared workflow names, five active statuses, or the strict + // the eight shared workflow names, five active statuses, or the strict // lower-ID predicate in this endpoint. Do not add provider filters or // early-stop on page order; a matching run may occur on a later page. // The residual cost is deep-history pagination, with retries restarting @@ -74648,7 +75489,7 @@ function isActivePreviousRun(run, query, workflowNames) { return typeof run.name === 'string' && workflowNames.includes(run.name) && run.id < query.currentRunId - && constants_1.WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); + && workflow_status_1.WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); } @@ -74856,6 +75697,36 @@ function firstNumericValue(...values) { } +/***/ }), + +/***/ 1462: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.WORKFLOW_ACTIVE_STATUSES = exports.WORKFLOW_STATUS = void 0; +exports.WORKFLOW_STATUS = { + IN_PROGRESS: 'in_progress', + QUEUED: 'queued', + REQUESTED: 'requested', + WAITING: 'waiting', + PENDING: 'pending', + COMPLETED: 'completed', + FAILED: 'failed', + CANCELLED: 'cancelled', + SKIPPED: 'skipped', + TIMED_OUT: 'timed_out', +}; +exports.WORKFLOW_ACTIVE_STATUSES = [ + exports.WORKFLOW_STATUS.IN_PROGRESS, + exports.WORKFLOW_STATUS.QUEUED, + exports.WORKFLOW_STATUS.REQUESTED, + exports.WORKFLOW_STATUS.WAITING, + exports.WORKFLOW_STATUS.PENDING, +]; + + /***/ }), /***/ 89040: @@ -75269,6 +76140,64 @@ function githubUsersMatch(left, right) { } +/***/ }), + +/***/ 38572: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.MAX_INACTIVITY_THRESHOLD_HOURS = exports.DEFAULT_INACTIVITY_THRESHOLD_HOURS = void 0; +exports.evaluateIssueInactivity = evaluateIssueInactivity; +/** Default inactivity window used by the scheduled issue-maintenance action. */ +exports.DEFAULT_INACTIVITY_THRESHOLD_HOURS = 168; +/** Maximum supported window (one year) for a finite, operationally useful value. */ +exports.MAX_INACTIVITY_THRESHOLD_HOURS = 8760; +/** + * Decides whether an issue can be closed without depending on GitHub or time + * APIs. GitHub's `updated_at` is treated as the last activity observed by the + * provider; this includes comments and issue metadata changes. + */ +function evaluateIssueInactivity(input) { + if (input.issue.isPullRequest) + return { kind: 'skip', reason: 'pull-request' }; + if (!hasLabel(input.issue.labels, input.waitingLabels)) { + return { kind: 'skip', reason: 'not-waiting' }; + } + if (hasLabel(input.issue.labels, [input.agentActivityLabel])) { + return { kind: 'skip', reason: 'agent-processing' }; + } + if (!Number.isFinite(input.thresholdHours) + || input.thresholdHours <= 0 + || input.thresholdHours > exports.MAX_INACTIVITY_THRESHOLD_HOURS) { + return { kind: 'skip', reason: 'invalid-threshold' }; + } + const updatedAtMilliseconds = Date.parse(input.issue.updatedAt ?? ''); + if (!Number.isFinite(updatedAtMilliseconds)) { + return { kind: 'skip', reason: 'missing-activity-timestamp' }; + } + if (!Number.isFinite(input.nowMilliseconds) || updatedAtMilliseconds > input.nowMilliseconds) { + return { kind: 'skip', reason: 'future-activity' }; + } + const inactiveForMilliseconds = input.nowMilliseconds - updatedAtMilliseconds; + const thresholdMilliseconds = input.thresholdHours * 60 * 60 * 1000; + return inactiveForMilliseconds >= thresholdMilliseconds + ? { kind: 'close', inactiveForMilliseconds } + : { kind: 'skip', reason: 'recent-activity' }; +} +function hasLabel(labels, candidates) { + const normalizedLabels = new Set(labels.map(normalize)); + return candidates.some(candidate => { + const normalizedCandidate = normalize(candidate); + return normalizedCandidate.length > 0 && normalizedLabels.has(normalizedCandidate); + }); +} +function normalize(value) { + return value.trim().toLowerCase(); +} + + /***/ }), /***/ 19879: @@ -75884,7 +76813,7 @@ exports.createRepositoryVariablesClient = createRepositoryVariablesClient; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createIssueTitleClient = exports.createIssueMetadataClient = exports.createIssueLifecycleClient = exports.createIssueLabelsClient = exports.createIssueLabelProvisioningClient = exports.createIssueContentClient = exports.createIssueAssignmentClient = void 0; +exports.createIssueTitleClient = exports.createIssueMetadataClient = exports.createIssueInactivityClient = exports.createIssueLifecycleClient = exports.createIssueLabelsClient = exports.createIssueLabelProvisioningClient = exports.createIssueContentClient = exports.createIssueAssignmentClient = void 0; const octokit_issue_adapters_1 = __nccwpck_require__(77179); const createIssueAssignmentClient = () => new octokit_issue_adapters_1.OctokitIssueAssignmentClientAdapter(); exports.createIssueAssignmentClient = createIssueAssignmentClient; @@ -75896,6 +76825,8 @@ const createIssueLabelsClient = () => new octokit_issue_adapters_1.OctokitIssueL exports.createIssueLabelsClient = createIssueLabelsClient; const createIssueLifecycleClient = () => new octokit_issue_adapters_1.OctokitIssueLifecycleClientAdapter(); exports.createIssueLifecycleClient = createIssueLifecycleClient; +const createIssueInactivityClient = () => new octokit_issue_adapters_1.OctokitIssueInactivityClientAdapter(); +exports.createIssueInactivityClient = createIssueInactivityClient; const createIssueMetadataClient = () => new octokit_issue_adapters_1.OctokitIssueMetadataClientAdapter(); exports.createIssueMetadataClient = createIssueMetadataClient; const createIssueTitleClient = () => new octokit_issue_adapters_1.OctokitIssueTitleClientAdapter(); @@ -76030,6 +76961,25 @@ function createIssueContentCompositionRoot() { } +/***/ }), + +/***/ 74914: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createCloseInactiveIssuesUseCase = createCloseInactiveIssuesUseCase; +const close_inactive_issues_use_case_1 = __nccwpck_require__(84579); +const issue_inactivity_repository_1 = __nccwpck_require__(28868); +const system_issue_inactivity_clock_adapter_1 = __nccwpck_require__(86457); +const github_issue_client_factory_1 = __nccwpck_require__(95883); +const issue_interaction_composition_root_1 = __nccwpck_require__(92503); +function createCloseInactiveIssuesUseCase() { + return new close_inactive_issues_use_case_1.CloseInactiveIssuesUseCase(new issue_inactivity_repository_1.IssueInactivityRepository((0, github_issue_client_factory_1.createIssueInactivityClient)()), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new system_issue_inactivity_clock_adapter_1.SystemIssueInactivityClockAdapter()); +} + + /***/ }), /***/ 92503: @@ -76263,6 +77213,7 @@ const pull_request_use_case_composition_root_1 = __nccwpck_require__(70636); const organization_members_composition_root_1 = __nccwpck_require__(50603); const update_pull_request_description_use_case_1 = __nccwpck_require__(75089); const pull_request_lifecycle_repository_1 = __nccwpck_require__(24189); +const issue_inactivity_composition_root_1 = __nccwpck_require__(74914); function createDetectPotentialProblemsUseCase() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); return new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)(), bugbot.context, bugbot.publication, bugbot.resolution); @@ -76271,7 +77222,7 @@ function createSingleActionUseCaseCompositionRoot() { const repositoryTagPort = new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()); const repositoryReleasePort = new repository_release_publication_repository_1.RepositoryReleasePublicationRepository((0, github_release_client_factory_1.createReleaseClient)()); const issueDescriptionQueryPort = (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(); - return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)())); + return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)()); } function createIssueCommentUseCaseCompositionRoot() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); @@ -76723,7 +77674,7 @@ exports.OctokitOwnerTypeClientAdapter = OctokitOwnerTypeClientAdapter; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.OctokitIssueTitleClientAdapter = exports.OctokitIssueMetadataClientAdapter = exports.OctokitIssueLifecycleClientAdapter = exports.OctokitIssueLabelsClientAdapter = exports.OctokitIssueLabelProvisioningClientAdapter = exports.OctokitIssueContentClientAdapter = exports.OctokitIssueAssignmentClientAdapter = void 0; +exports.OctokitIssueTitleClientAdapter = exports.OctokitIssueMetadataClientAdapter = exports.OctokitIssueInactivityClientAdapter = exports.OctokitIssueLifecycleClientAdapter = exports.OctokitIssueLabelsClientAdapter = exports.OctokitIssueLabelProvisioningClientAdapter = exports.OctokitIssueContentClientAdapter = exports.OctokitIssueAssignmentClientAdapter = void 0; const octokit_client_resolver_1 = __nccwpck_require__(54047); class OctokitIssueAssignmentClientAdapter { getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } @@ -76745,6 +77696,10 @@ class OctokitIssueLifecycleClientAdapter { getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } } exports.OctokitIssueLifecycleClientAdapter = OctokitIssueLifecycleClientAdapter; +class OctokitIssueInactivityClientAdapter { + getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } +} +exports.OctokitIssueInactivityClientAdapter = OctokitIssueInactivityClientAdapter; class OctokitIssueMetadataClientAdapter { getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } } @@ -77305,6 +78260,23 @@ class SetupWorkspaceAdapter { exports.SetupWorkspaceAdapter = SetupWorkspaceAdapter; +/***/ }), + +/***/ 86457: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SystemIssueInactivityClockAdapter = void 0; +class SystemIssueInactivityClockAdapter { + nowMilliseconds() { + return Date.now(); + } +} +exports.SystemIssueInactivityClockAdapter = SystemIssueInactivityClockAdapter; + + /***/ }), /***/ 32679: @@ -78265,429 +79237,6 @@ function stripTrailingCommentWatermarks(comment) { } -/***/ }), - -/***/ 15415: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.PROMPTS = exports.BUGBOT_MIN_SEVERITY = exports.BUGBOT_MAX_COMMENTS = exports.BUGBOT_MARKER_PREFIX = exports.ACTIONS = exports.ERRORS = exports.INPUT_KEYS = exports.WORKFLOW_ACTIVE_STATUSES = exports.WORKFLOW_STATUS = exports.DEFAULT_IMAGE_CONFIG = exports.AGENT_REQUEST_TIMEOUT_MS = exports.TITLE = void 0; -exports.TITLE = 'Copilot'; -/** Maximum time allowed for one external agent CLI request. */ -exports.AGENT_REQUEST_TIMEOUT_MS = 900000; -exports.DEFAULT_IMAGE_CONFIG = { - issue: { - automatic: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp" - ], - feature: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", - ], - bugfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" - ], - hotfix: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" - ], - release: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", - ], - docs: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", - ], - chore: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", - ], - }, - pullRequest: { - automatic: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - ], - feature: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", - ], - bugfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", - ], - hotfix: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", - ], - release: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", - ], - docs: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", - ], - chore: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", - ], - }, - commit: { - automatic: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - feature: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - bugfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - hotfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - release: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - docs: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - chore: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ] - } -}; -exports.WORKFLOW_STATUS = { - IN_PROGRESS: 'in_progress', - QUEUED: 'queued', - REQUESTED: 'requested', - WAITING: 'waiting', - PENDING: 'pending', - COMPLETED: 'completed', - FAILED: 'failed', - CANCELLED: 'cancelled', - SKIPPED: 'skipped', - TIMED_OUT: 'timed_out', -}; -exports.WORKFLOW_ACTIVE_STATUSES = [ - exports.WORKFLOW_STATUS.IN_PROGRESS, - exports.WORKFLOW_STATUS.QUEUED, - exports.WORKFLOW_STATUS.REQUESTED, - exports.WORKFLOW_STATUS.WAITING, - exports.WORKFLOW_STATUS.PENDING, -]; -exports.INPUT_KEYS = { - // Debug - DEBUG: 'debug', - // Welcome - WELCOME_TITLE: 'welcome-title', - WELCOME_MESSAGES: 'welcome-messages', - // Single action - SINGLE_ACTION: 'single-action', - SINGLE_ACTION_ISSUE: 'single-action-issue', - SINGLE_ACTION_VERSION: 'single-action-version', - SINGLE_ACTION_TITLE: 'single-action-title', - SINGLE_ACTION_CHANGELOG: 'single-action-changelog', - // Tokens - TOKEN: 'token', - QUEUE_GATE_ONLY: 'queue-gate-only', - // Agent selection - AGENT_PROVIDER: 'agent-provider', - AGENT_MODEL_PROVIDER: 'agent-model-provider', - AGENT_EFFORT: 'agent-effort', - AGENT_MODEL: 'agent-model', - AGENT_COMMAND: 'agent-command', - FINDINGS_PROVIDER: 'findings-provider', - FINDINGS_MODEL_PROVIDER: 'findings-model-provider', - FINDINGS_EFFORT: 'findings-effort', - FINDINGS_MODEL: 'findings-model', - FINDINGS_COMMAND: 'findings-command', - FIXER_PROVIDER: 'fixer-provider', - FIXER_MODEL_PROVIDER: 'fixer-model-provider', - FIXER_EFFORT: 'fixer-effort', - FIXER_MODEL: 'fixer-model', - FIXER_COMMAND: 'fixer-command', - PLANNER_PROVIDER: 'planner-provider', - PLANNER_MODEL_PROVIDER: 'planner-model-provider', - PLANNER_EFFORT: 'planner-effort', - PLANNER_MODEL: 'planner-model', - PLANNER_COMMAND: 'planner-command', - REVIEWER_PROVIDER: 'reviewer-provider', - REVIEWER_MODEL_PROVIDER: 'reviewer-model-provider', - REVIEWER_EFFORT: 'reviewer-effort', - REVIEWER_MODEL: 'reviewer-model', - REVIEWER_COMMAND: 'reviewer-command', - TESTER_PROVIDER: 'tester-provider', - TESTER_MODEL_PROVIDER: 'tester-model-provider', - TESTER_EFFORT: 'tester-effort', - TESTER_MODEL: 'tester-model', - TESTER_COMMAND: 'tester-command', - RELEASE_PROVIDER: 'release-provider', - RELEASE_MODEL_PROVIDER: 'release-model-provider', - RELEASE_EFFORT: 'release-effort', - RELEASE_MODEL: 'release-model', - RELEASE_COMMAND: 'release-command', - // AI configuration - AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', - AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', - AI_MEMBERS_ONLY: 'ai-members-only', - AI_IGNORE_FILES: 'ai-ignore-files', - AI_INCLUDE_REASONING: 'ai-include-reasoning', - BUGBOT_SEVERITY: 'bugbot-severity', - BUGBOT_COMMENT_LIMIT: 'bugbot-comment-limit', - BUGBOT_FIX_VERIFY_COMMANDS: 'bugbot-fix-verify-commands', - // Projects - PROJECT_IDS: 'project-ids', - PROJECT_COLUMN_ISSUE_CREATED: 'project-column-issue-created', - PROJECT_COLUMN_PULL_REQUEST_CREATED: 'project-column-pull-request-created', - PROJECT_COLUMN_ISSUE_IN_PROGRESS: 'project-column-issue-in-progress', - PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: 'project-column-pull-request-in-progress', - // Images - IMAGES_ON_ISSUE: 'images-on-issue', - IMAGES_ON_PULL_REQUEST: 'images-on-pull-request', - IMAGES_ON_COMMIT: 'images-on-commit', - IMAGES_ISSUE_AUTOMATIC: 'images-issue-automatic', - IMAGES_ISSUE_FEATURE: 'images-issue-feature', - IMAGES_ISSUE_BUGFIX: 'images-issue-bugfix', - IMAGES_ISSUE_DOCS: 'images-issue-docs', - IMAGES_ISSUE_CHORE: 'images-issue-chore', - IMAGES_ISSUE_RELEASE: 'images-issue-release', - IMAGES_ISSUE_HOTFIX: 'images-issue-hotfix', - IMAGES_PULL_REQUEST_AUTOMATIC: 'images-pull-request-automatic', - IMAGES_PULL_REQUEST_FEATURE: 'images-pull-request-feature', - IMAGES_PULL_REQUEST_BUGFIX: 'images-pull-request-bugfix', - IMAGES_PULL_REQUEST_RELEASE: 'images-pull-request-release', - IMAGES_PULL_REQUEST_HOTFIX: 'images-pull-request-hotfix', - IMAGES_PULL_REQUEST_DOCS: 'images-pull-request-docs', - IMAGES_PULL_REQUEST_CHORE: 'images-pull-request-chore', - IMAGES_COMMIT_AUTOMATIC: 'images-commit-automatic', - IMAGES_COMMIT_FEATURE: 'images-commit-feature', - IMAGES_COMMIT_BUGFIX: 'images-commit-bugfix', - IMAGES_COMMIT_RELEASE: 'images-commit-release', - IMAGES_COMMIT_HOTFIX: 'images-commit-hotfix', - IMAGES_COMMIT_DOCS: 'images-commit-docs', - IMAGES_COMMIT_CHORE: 'images-commit-chore', - // Workflows - RELEASE_WORKFLOW: 'release-workflow', - HOTFIX_WORKFLOW: 'hotfix-workflow', - // Emoji - EMOJI_LABELED_TITLE: 'emoji-labeled-title', - BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', - // Labels - BRANCH_MANAGEMENT_LAUNCHER_LABEL: 'branch-management-launcher-label', - BUGFIX_LABEL: 'bugfix-label', - BUG_LABEL: 'bug-label', - HOTFIX_LABEL: 'hotfix-label', - ENHANCEMENT_LABEL: 'enhancement-label', - FEATURE_LABEL: 'feature-label', - RELEASE_LABEL: 'release-label', - QUESTION_LABEL: 'question-label', - HELP_LABEL: 'help-label', - DEPLOY_LABEL: 'deploy-label', - DEPLOYED_LABEL: 'deployed-label', - DOCS_LABEL: 'docs-label', - DOCUMENTATION_LABEL: 'documentation-label', - CHORE_LABEL: 'chore-label', - MAINTENANCE_LABEL: 'maintenance-label', - PRIORITY_HIGH_LABEL: 'priority-high-label', - PRIORITY_MEDIUM_LABEL: 'priority-medium-label', - PRIORITY_LOW_LABEL: 'priority-low-label', - PRIORITY_NONE_LABEL: 'priority-none-label', - SIZE_XXL_LABEL: 'size-xxl-label', - SIZE_XL_LABEL: 'size-xl-label', - SIZE_L_LABEL: 'size-l-label', - SIZE_M_LABEL: 'size-m-label', - SIZE_S_LABEL: 'size-s-label', - SIZE_XS_LABEL: 'size-xs-label', - // Lifecycle label inputs - STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', - STATE_PLANNED_LABEL: 'state-planned-label', - STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', - STATE_REVIEWING_LABEL: 'state-reviewing-label', - STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', - STATE_VERIFIED_LABEL: 'state-verified-label', - STATE_READY_LABEL: 'state-ready-label', - STATE_BLOCKED_LABEL: 'state-blocked-label', - STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', - STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', - // Issue Types - ISSUE_TYPE_BUG: 'issue-type-bug', - ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', - ISSUE_TYPE_BUG_COLOR: 'issue-type-bug-color', - ISSUE_TYPE_HOTFIX: 'issue-type-hotfix', - ISSUE_TYPE_HOTFIX_DESCRIPTION: 'issue-type-hotfix-description', - ISSUE_TYPE_HOTFIX_COLOR: 'issue-type-hotfix-color', - ISSUE_TYPE_FEATURE: 'issue-type-feature', - ISSUE_TYPE_FEATURE_DESCRIPTION: 'issue-type-feature-description', - ISSUE_TYPE_FEATURE_COLOR: 'issue-type-feature-color', - ISSUE_TYPE_DOCUMENTATION: 'issue-type-documentation', - ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: 'issue-type-documentation-description', - ISSUE_TYPE_DOCUMENTATION_COLOR: 'issue-type-documentation-color', - ISSUE_TYPE_MAINTENANCE: 'issue-type-maintenance', - ISSUE_TYPE_MAINTENANCE_DESCRIPTION: 'issue-type-maintenance-description', - ISSUE_TYPE_MAINTENANCE_COLOR: 'issue-type-maintenance-color', - ISSUE_TYPE_RELEASE: 'issue-type-release', - ISSUE_TYPE_RELEASE_DESCRIPTION: 'issue-type-release-description', - ISSUE_TYPE_RELEASE_COLOR: 'issue-type-release-color', - ISSUE_TYPE_QUESTION: 'issue-type-question', - ISSUE_TYPE_QUESTION_DESCRIPTION: 'issue-type-question-description', - ISSUE_TYPE_QUESTION_COLOR: 'issue-type-question-color', - ISSUE_TYPE_HELP: 'issue-type-help', - ISSUE_TYPE_HELP_DESCRIPTION: 'issue-type-help-description', - ISSUE_TYPE_HELP_COLOR: 'issue-type-help-color', - ISSUE_TYPE_TASK: 'issue-type-task', - ISSUE_TYPE_TASK_DESCRIPTION: 'issue-type-task-description', - ISSUE_TYPE_TASK_COLOR: 'issue-type-task-color', - // Locale - ISSUES_LOCALE: 'issues-locale', - PULL_REQUESTS_LOCALE: 'pull-requests-locale', - // Size Thresholds - SIZE_XXL_THRESHOLD_LINES: 'size-xxl-threshold-lines', - SIZE_XXL_THRESHOLD_FILES: 'size-xxl-threshold-files', - SIZE_XXL_THRESHOLD_COMMITS: 'size-xxl-threshold-commits', - SIZE_XL_THRESHOLD_LINES: 'size-xl-threshold-lines', - SIZE_XL_THRESHOLD_FILES: 'size-xl-threshold-files', - SIZE_XL_THRESHOLD_COMMITS: 'size-xl-threshold-commits', - SIZE_L_THRESHOLD_LINES: 'size-l-threshold-lines', - SIZE_L_THRESHOLD_FILES: 'size-l-threshold-files', - SIZE_L_THRESHOLD_COMMITS: 'size-l-threshold-commits', - SIZE_M_THRESHOLD_LINES: 'size-m-threshold-lines', - SIZE_M_THRESHOLD_FILES: 'size-m-threshold-files', - SIZE_M_THRESHOLD_COMMITS: 'size-m-threshold-commits', - SIZE_S_THRESHOLD_LINES: 'size-s-threshold-lines', - SIZE_S_THRESHOLD_FILES: 'size-s-threshold-files', - SIZE_S_THRESHOLD_COMMITS: 'size-s-threshold-commits', - SIZE_XS_THRESHOLD_LINES: 'size-xs-threshold-lines', - SIZE_XS_THRESHOLD_FILES: 'size-xs-threshold-files', - SIZE_XS_THRESHOLD_COMMITS: 'size-xs-threshold-commits', - // Branches - MAIN_BRANCH: 'main-branch', - DEVELOPMENT_BRANCH: 'development-branch', - FEATURE_TREE: 'feature-tree', - BUGFIX_TREE: 'bugfix-tree', - HOTFIX_TREE: 'hotfix-tree', - RELEASE_TREE: 'release-tree', - DOCS_TREE: 'docs-tree', - CHORE_TREE: 'chore-tree', - // Commit - COMMIT_PREFIX_TRANSFORMS: 'commit-prefix-transforms', - // Issue - BRANCH_MANAGEMENT_ALWAYS: 'branch-management-always', - REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', - DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', - // Pull Request - PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', - PULL_REQUEST_DESIRED_REVIEWERS_COUNT: 'desired-reviewers-count', - PULL_REQUEST_MERGE_TIMEOUT: 'merge-timeout', -}; -exports.ERRORS = { - GIT_REPOSITORY_NOT_FOUND: '❌ Git repository not found' -}; -var action_types_1 = __nccwpck_require__(19625); -Object.defineProperty(exports, "ACTIONS", ({ enumerable: true, get: function () { return action_types_1.ACTIONS; } })); -/** Hidden HTML comment prefix for bugbot findings (issue/PR comments). Format: */ -exports.BUGBOT_MARKER_PREFIX = 'copilot-bugbot'; -/** Max number of individual bugbot comments to create per issue/PR. Excess findings get one summary comment suggesting to review locally. */ -exports.BUGBOT_MAX_COMMENTS = 20; -/** Minimum severity to publish (findings below this are dropped). Order: high > medium > low > info. */ -exports.BUGBOT_MIN_SEVERITY = 'low'; -exports.PROMPTS = {}; - - /***/ }), /***/ 92816: @@ -79140,6 +79689,7 @@ function copySetupFiles(cwd, setupDirOverride, features, options = {}) { 'hotfix_workflow.yml': 'hotfix', 'agent-cli-provisioning.yml': 'agentProvisioning', 'copilot_credential_health.yml': 'credentialHealth', + 'copilot_close_inactive_issues.yml': 'inactiveIssueClosure', }; const approvedWorkflowFiles = new Set(options.approvedWorkflowFiles ?? []); const backupDirectory = options.updateExistingWorkflows ? path.join(cwd, '.copilot', 'setup-backups', new Date().toISOString().replace(/[:.]/g, '-')) : undefined; @@ -79174,6 +79724,7 @@ function compareSetupWorkflows(cwd, features, setupDirOverride) { 'hotfix_workflow.yml': 'hotfix', 'agent-cli-provisioning.yml': 'agentProvisioning', 'copilot_credential_health.yml': 'credentialHealth', + 'copilot_close_inactive_issues.yml': 'inactiveIssueClosure', }; const sourceDirectory = path.join(setupDir, 'workflows'); if (!fs.existsSync(sourceDirectory)) diff --git a/build/cli/src/actions/default_image_config.d.ts b/build/cli/src/actions/default_image_config.d.ts new file mode 100644 index 00000000..8d656ad3 --- /dev/null +++ b/build/cli/src/actions/default_image_config.d.ts @@ -0,0 +1,30 @@ +/** Default illustration URLs used when an action does not receive custom images. */ +export declare const DEFAULT_IMAGE_CONFIG: { + issue: { + automatic: string[]; + feature: string[]; + bugfix: string[]; + hotfix: string[]; + release: string[]; + docs: string[]; + chore: string[]; + }; + pullRequest: { + automatic: string[]; + feature: string[]; + bugfix: string[]; + hotfix: string[]; + release: string[]; + docs: string[]; + chore: string[]; + }; + commit: { + automatic: string[]; + feature: string[]; + bugfix: string[]; + hotfix: string[]; + release: string[]; + docs: string[]; + chore: string[]; + }; +}; diff --git a/build/cli/src/actions/image_configuration_builder.d.ts b/build/cli/src/actions/image_configuration_builder.d.ts index e815dd30..e17cb825 100644 --- a/build/cli/src/actions/image_configuration_builder.d.ts +++ b/build/cli/src/actions/image_configuration_builder.d.ts @@ -1,4 +1,4 @@ -import { DEFAULT_IMAGE_CONFIG } from '../utils/constants'; +import { DEFAULT_IMAGE_CONFIG } from './default_image_config'; export type ImageConfigurationReader = (key: string) => unknown; type ImageGroup = keyof typeof DEFAULT_IMAGE_CONFIG; type ImageVariant = keyof (typeof DEFAULT_IMAGE_CONFIG)[ImageGroup]; diff --git a/build/cli/src/actions/local_action_configuration.d.ts b/build/cli/src/actions/local_action_configuration.d.ts index f6ab15af..93a04530 100644 --- a/build/cli/src/actions/local_action_configuration.d.ts +++ b/build/cli/src/actions/local_action_configuration.d.ts @@ -134,6 +134,7 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn singleActionVersion: string; singleActionTitle: string; singleActionChangelog: string; + inactivityThresholdHours: number; token: string; }>; export type LocalActionConfiguration = Awaited>; diff --git a/build/cli/src/actions/local_action_configuration_sections.d.ts b/build/cli/src/actions/local_action_configuration_sections.d.ts index 7b0d3a20..aad4e03d 100644 --- a/build/cli/src/actions/local_action_configuration_sections.d.ts +++ b/build/cli/src/actions/local_action_configuration_sections.d.ts @@ -12,6 +12,7 @@ export declare function readLocalCoreConfiguration(additionalParams: ActionInput singleActionVersion: string; singleActionTitle: string; singleActionChangelog: string; + inactivityThresholdHours: number; token: string; }; export declare function readLocalAgentConfiguration(additionalParams: ActionInputValues, actionInputs: LocalActionInputs): { diff --git a/build/cli/src/application/contracts/input_keys.d.ts b/build/cli/src/application/contracts/input_keys.d.ts new file mode 100644 index 00000000..af1b6a5e --- /dev/null +++ b/build/cli/src/application/contracts/input_keys.d.ts @@ -0,0 +1,187 @@ +/** Canonical action and CLI input vocabulary shared by input mappers. */ +export declare const INPUT_KEYS: { + readonly DEBUG: "debug"; + readonly WELCOME_TITLE: "welcome-title"; + readonly WELCOME_MESSAGES: "welcome-messages"; + readonly SINGLE_ACTION: "single-action"; + readonly SINGLE_ACTION_ISSUE: "single-action-issue"; + readonly SINGLE_ACTION_VERSION: "single-action-version"; + readonly SINGLE_ACTION_TITLE: "single-action-title"; + readonly SINGLE_ACTION_CHANGELOG: "single-action-changelog"; + readonly INACTIVITY_THRESHOLD_HOURS: "inactivity-threshold-hours"; + readonly TOKEN: "token"; + readonly QUEUE_GATE_ONLY: "queue-gate-only"; + readonly AGENT_PROVIDER: "agent-provider"; + readonly AGENT_MODEL_PROVIDER: "agent-model-provider"; + readonly AGENT_EFFORT: "agent-effort"; + readonly AGENT_MODEL: "agent-model"; + readonly AGENT_COMMAND: "agent-command"; + readonly FINDINGS_PROVIDER: "findings-provider"; + readonly FINDINGS_MODEL_PROVIDER: "findings-model-provider"; + readonly FINDINGS_EFFORT: "findings-effort"; + readonly FINDINGS_MODEL: "findings-model"; + readonly FINDINGS_COMMAND: "findings-command"; + readonly FIXER_PROVIDER: "fixer-provider"; + readonly FIXER_MODEL_PROVIDER: "fixer-model-provider"; + readonly FIXER_EFFORT: "fixer-effort"; + readonly FIXER_MODEL: "fixer-model"; + readonly FIXER_COMMAND: "fixer-command"; + readonly PLANNER_PROVIDER: "planner-provider"; + readonly PLANNER_MODEL_PROVIDER: "planner-model-provider"; + readonly PLANNER_EFFORT: "planner-effort"; + readonly PLANNER_MODEL: "planner-model"; + readonly PLANNER_COMMAND: "planner-command"; + readonly REVIEWER_PROVIDER: "reviewer-provider"; + readonly REVIEWER_MODEL_PROVIDER: "reviewer-model-provider"; + readonly REVIEWER_EFFORT: "reviewer-effort"; + readonly REVIEWER_MODEL: "reviewer-model"; + readonly REVIEWER_COMMAND: "reviewer-command"; + readonly TESTER_PROVIDER: "tester-provider"; + readonly TESTER_MODEL_PROVIDER: "tester-model-provider"; + readonly TESTER_EFFORT: "tester-effort"; + readonly TESTER_MODEL: "tester-model"; + readonly TESTER_COMMAND: "tester-command"; + readonly RELEASE_PROVIDER: "release-provider"; + readonly RELEASE_MODEL_PROVIDER: "release-model-provider"; + readonly RELEASE_EFFORT: "release-effort"; + readonly RELEASE_MODEL: "release-model"; + readonly RELEASE_COMMAND: "release-command"; + readonly AI_PULL_REQUEST_DESCRIPTION: "ai-pull-request-description"; + readonly AI_PULL_REQUEST_DESCRIPTION_MODE: "ai-pull-request-description-mode"; + readonly AI_MEMBERS_ONLY: "ai-members-only"; + readonly AI_IGNORE_FILES: "ai-ignore-files"; + readonly AI_INCLUDE_REASONING: "ai-include-reasoning"; + readonly BUGBOT_SEVERITY: "bugbot-severity"; + readonly BUGBOT_COMMENT_LIMIT: "bugbot-comment-limit"; + readonly BUGBOT_FIX_VERIFY_COMMANDS: "bugbot-fix-verify-commands"; + readonly PROJECT_IDS: "project-ids"; + readonly PROJECT_COLUMN_ISSUE_CREATED: "project-column-issue-created"; + readonly PROJECT_COLUMN_PULL_REQUEST_CREATED: "project-column-pull-request-created"; + readonly PROJECT_COLUMN_ISSUE_IN_PROGRESS: "project-column-issue-in-progress"; + readonly PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: "project-column-pull-request-in-progress"; + readonly IMAGES_ON_ISSUE: "images-on-issue"; + readonly IMAGES_ON_PULL_REQUEST: "images-on-pull-request"; + readonly IMAGES_ON_COMMIT: "images-on-commit"; + readonly IMAGES_ISSUE_AUTOMATIC: "images-issue-automatic"; + readonly IMAGES_ISSUE_FEATURE: "images-issue-feature"; + readonly IMAGES_ISSUE_BUGFIX: "images-issue-bugfix"; + readonly IMAGES_ISSUE_DOCS: "images-issue-docs"; + readonly IMAGES_ISSUE_CHORE: "images-issue-chore"; + readonly IMAGES_ISSUE_RELEASE: "images-issue-release"; + readonly IMAGES_ISSUE_HOTFIX: "images-issue-hotfix"; + readonly IMAGES_PULL_REQUEST_AUTOMATIC: "images-pull-request-automatic"; + readonly IMAGES_PULL_REQUEST_FEATURE: "images-pull-request-feature"; + readonly IMAGES_PULL_REQUEST_BUGFIX: "images-pull-request-bugfix"; + readonly IMAGES_PULL_REQUEST_RELEASE: "images-pull-request-release"; + readonly IMAGES_PULL_REQUEST_HOTFIX: "images-pull-request-hotfix"; + readonly IMAGES_PULL_REQUEST_DOCS: "images-pull-request-docs"; + readonly IMAGES_PULL_REQUEST_CHORE: "images-pull-request-chore"; + readonly IMAGES_COMMIT_AUTOMATIC: "images-commit-automatic"; + readonly IMAGES_COMMIT_FEATURE: "images-commit-feature"; + readonly IMAGES_COMMIT_BUGFIX: "images-commit-bugfix"; + readonly IMAGES_COMMIT_RELEASE: "images-commit-release"; + readonly IMAGES_COMMIT_HOTFIX: "images-commit-hotfix"; + readonly IMAGES_COMMIT_DOCS: "images-commit-docs"; + readonly IMAGES_COMMIT_CHORE: "images-commit-chore"; + readonly RELEASE_WORKFLOW: "release-workflow"; + readonly HOTFIX_WORKFLOW: "hotfix-workflow"; + readonly EMOJI_LABELED_TITLE: "emoji-labeled-title"; + readonly BRANCH_MANAGEMENT_EMOJI: "branch-management-emoji"; + readonly BRANCH_MANAGEMENT_LAUNCHER_LABEL: "branch-management-launcher-label"; + readonly BUGFIX_LABEL: "bugfix-label"; + readonly BUG_LABEL: "bug-label"; + readonly HOTFIX_LABEL: "hotfix-label"; + readonly ENHANCEMENT_LABEL: "enhancement-label"; + readonly FEATURE_LABEL: "feature-label"; + readonly RELEASE_LABEL: "release-label"; + readonly QUESTION_LABEL: "question-label"; + readonly HELP_LABEL: "help-label"; + readonly DEPLOY_LABEL: "deploy-label"; + readonly DEPLOYED_LABEL: "deployed-label"; + readonly DOCS_LABEL: "docs-label"; + readonly DOCUMENTATION_LABEL: "documentation-label"; + readonly CHORE_LABEL: "chore-label"; + readonly MAINTENANCE_LABEL: "maintenance-label"; + readonly PRIORITY_HIGH_LABEL: "priority-high-label"; + readonly PRIORITY_MEDIUM_LABEL: "priority-medium-label"; + readonly PRIORITY_LOW_LABEL: "priority-low-label"; + readonly PRIORITY_NONE_LABEL: "priority-none-label"; + readonly SIZE_XXL_LABEL: "size-xxl-label"; + readonly SIZE_XL_LABEL: "size-xl-label"; + readonly SIZE_L_LABEL: "size-l-label"; + readonly SIZE_M_LABEL: "size-m-label"; + readonly SIZE_S_LABEL: "size-s-label"; + readonly SIZE_XS_LABEL: "size-xs-label"; + readonly STATE_AI_PROCESSING_LABEL: "state-ai-processing-label"; + readonly STATE_PLANNED_LABEL: "state-planned-label"; + readonly STATE_IN_PROGRESS_LABEL: "state-in-progress-label"; + readonly STATE_REVIEWING_LABEL: "state-reviewing-label"; + readonly STATE_CHANGES_REQUESTED_LABEL: "state-changes-requested-label"; + readonly STATE_VERIFIED_LABEL: "state-verified-label"; + readonly STATE_READY_LABEL: "state-ready-label"; + readonly STATE_BLOCKED_LABEL: "state-blocked-label"; + readonly STATE_AWAITING_MAINTAINER_LABEL: "state-awaiting-maintainer-label"; + readonly STATE_AWAITING_ISSUE_AUTHOR_LABEL: "state-awaiting-issue-author-label"; + readonly ISSUE_TYPE_BUG: "issue-type-bug"; + readonly ISSUE_TYPE_BUG_DESCRIPTION: "issue-type-bug-description"; + readonly ISSUE_TYPE_BUG_COLOR: "issue-type-bug-color"; + readonly ISSUE_TYPE_HOTFIX: "issue-type-hotfix"; + readonly ISSUE_TYPE_HOTFIX_DESCRIPTION: "issue-type-hotfix-description"; + readonly ISSUE_TYPE_HOTFIX_COLOR: "issue-type-hotfix-color"; + readonly ISSUE_TYPE_FEATURE: "issue-type-feature"; + readonly ISSUE_TYPE_FEATURE_DESCRIPTION: "issue-type-feature-description"; + readonly ISSUE_TYPE_FEATURE_COLOR: "issue-type-feature-color"; + readonly ISSUE_TYPE_DOCUMENTATION: "issue-type-documentation"; + readonly ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: "issue-type-documentation-description"; + readonly ISSUE_TYPE_DOCUMENTATION_COLOR: "issue-type-documentation-color"; + readonly ISSUE_TYPE_MAINTENANCE: "issue-type-maintenance"; + readonly ISSUE_TYPE_MAINTENANCE_DESCRIPTION: "issue-type-maintenance-description"; + readonly ISSUE_TYPE_MAINTENANCE_COLOR: "issue-type-maintenance-color"; + readonly ISSUE_TYPE_RELEASE: "issue-type-release"; + readonly ISSUE_TYPE_RELEASE_DESCRIPTION: "issue-type-release-description"; + readonly ISSUE_TYPE_RELEASE_COLOR: "issue-type-release-color"; + readonly ISSUE_TYPE_QUESTION: "issue-type-question"; + readonly ISSUE_TYPE_QUESTION_DESCRIPTION: "issue-type-question-description"; + readonly ISSUE_TYPE_QUESTION_COLOR: "issue-type-question-color"; + readonly ISSUE_TYPE_HELP: "issue-type-help"; + readonly ISSUE_TYPE_HELP_DESCRIPTION: "issue-type-help-description"; + readonly ISSUE_TYPE_HELP_COLOR: "issue-type-help-color"; + readonly ISSUE_TYPE_TASK: "issue-type-task"; + readonly ISSUE_TYPE_TASK_DESCRIPTION: "issue-type-task-description"; + readonly ISSUE_TYPE_TASK_COLOR: "issue-type-task-color"; + readonly ISSUES_LOCALE: "issues-locale"; + readonly PULL_REQUESTS_LOCALE: "pull-requests-locale"; + readonly SIZE_XXL_THRESHOLD_LINES: "size-xxl-threshold-lines"; + readonly SIZE_XXL_THRESHOLD_FILES: "size-xxl-threshold-files"; + readonly SIZE_XXL_THRESHOLD_COMMITS: "size-xxl-threshold-commits"; + readonly SIZE_XL_THRESHOLD_LINES: "size-xl-threshold-lines"; + readonly SIZE_XL_THRESHOLD_FILES: "size-xl-threshold-files"; + readonly SIZE_XL_THRESHOLD_COMMITS: "size-xl-threshold-commits"; + readonly SIZE_L_THRESHOLD_LINES: "size-l-threshold-lines"; + readonly SIZE_L_THRESHOLD_FILES: "size-l-threshold-files"; + readonly SIZE_L_THRESHOLD_COMMITS: "size-l-threshold-commits"; + readonly SIZE_M_THRESHOLD_LINES: "size-m-threshold-lines"; + readonly SIZE_M_THRESHOLD_FILES: "size-m-threshold-files"; + readonly SIZE_M_THRESHOLD_COMMITS: "size-m-threshold-commits"; + readonly SIZE_S_THRESHOLD_LINES: "size-s-threshold-lines"; + readonly SIZE_S_THRESHOLD_FILES: "size-s-threshold-files"; + readonly SIZE_S_THRESHOLD_COMMITS: "size-s-threshold-commits"; + readonly SIZE_XS_THRESHOLD_LINES: "size-xs-threshold-lines"; + readonly SIZE_XS_THRESHOLD_FILES: "size-xs-threshold-files"; + readonly SIZE_XS_THRESHOLD_COMMITS: "size-xs-threshold-commits"; + readonly MAIN_BRANCH: "main-branch"; + readonly DEVELOPMENT_BRANCH: "development-branch"; + readonly FEATURE_TREE: "feature-tree"; + readonly BUGFIX_TREE: "bugfix-tree"; + readonly HOTFIX_TREE: "hotfix-tree"; + readonly RELEASE_TREE: "release-tree"; + readonly DOCS_TREE: "docs-tree"; + readonly CHORE_TREE: "chore-tree"; + readonly COMMIT_PREFIX_TRANSFORMS: "commit-prefix-transforms"; + readonly BRANCH_MANAGEMENT_ALWAYS: "branch-management-always"; + readonly REOPEN_ISSUE_ON_PUSH: "reopen-issue-on-push"; + readonly DESIRED_ASSIGNEES_COUNT: "desired-assignees-count"; + readonly PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: "desired-assignees-count"; + readonly PULL_REQUEST_DESIRED_REVIEWERS_COUNT: "desired-reviewers-count"; + readonly PULL_REQUEST_MERGE_TIMEOUT: "merge-timeout"; +}; diff --git a/build/cli/src/application/contracts/product_identity.d.ts b/build/cli/src/application/contracts/product_identity.d.ts new file mode 100644 index 00000000..9ed260e6 --- /dev/null +++ b/build/cli/src/application/contracts/product_identity.d.ts @@ -0,0 +1 @@ +export declare const TITLE = "Copilot"; diff --git a/build/cli/src/application/policies/bugbot_constants.d.ts b/build/cli/src/application/policies/bugbot_constants.d.ts new file mode 100644 index 00000000..d43484a4 --- /dev/null +++ b/build/cli/src/application/policies/bugbot_constants.d.ts @@ -0,0 +1,6 @@ +/** Hidden marker prefix used to reconcile Bugbot findings across comments. */ +export declare const BUGBOT_MARKER_PREFIX = "copilot-bugbot"; +/** Maximum number of individual Bugbot comments published for one analysis. */ +export declare const BUGBOT_MAX_COMMENTS = 20; +/** Minimum severity published by default. */ +export declare const BUGBOT_MIN_SEVERITY: 'info' | 'low' | 'medium' | 'high'; diff --git a/build/cli/src/application/policies/setup_configuration_defaults.d.ts b/build/cli/src/application/policies/setup_configuration_defaults.d.ts new file mode 100644 index 00000000..484a150d --- /dev/null +++ b/build/cli/src/application/policies/setup_configuration_defaults.d.ts @@ -0,0 +1,22 @@ +import type { AgentTask } from '../../domain/agent'; +import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupResourceStoragePolicy, SetupStorageConfiguration } from '../../domain/setup'; +export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; +export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; +export declare function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration; +export declare function createDefaultSetupConfiguration(): SetupConfiguration; +export type SetupConfigurationOverrides = { + features?: Partial; + agents?: Partial>>; + repository?: Partial; + ai?: Partial; + projects?: Partial; + createInitialTag?: boolean; + manageRepositoryVariables?: boolean; + manageRepositorySecrets?: boolean; + actionInputs?: Record; + storage?: { + secrets?: Partial; + variables?: Partial; + }; +}; +export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; diff --git a/build/cli/src/application/policies/setup_configuration_plan.d.ts b/build/cli/src/application/policies/setup_configuration_plan.d.ts new file mode 100644 index 00000000..4314ee38 --- /dev/null +++ b/build/cli/src/application/policies/setup_configuration_plan.d.ts @@ -0,0 +1,6 @@ +import type { SetupConfiguration, SetupCredentialRequirement, SetupPlan, SetupVariable } from '../../domain/setup'; +export declare function buildSetupPlan(configuration: SetupConfiguration): SetupPlan; +/** Builds the non-sensitive credential contract implied by the selected agents. */ +export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; +export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; +export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; diff --git a/build/cli/src/application/policies/setup_configuration_policy.d.ts b/build/cli/src/application/policies/setup_configuration_policy.d.ts index 67ef0424..b23fda05 100644 --- a/build/cli/src/application/policies/setup_configuration_policy.d.ts +++ b/build/cli/src/application/policies/setup_configuration_policy.d.ts @@ -1,41 +1,5 @@ -import type { AgentTask } from '../../domain/agent'; -import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement, SetupResourceScope, SetupResourceStoragePolicy, SetupStorageConfiguration, SetupRemoteConfiguration, SetupResourceTarget } from '../../domain/setup'; -export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; -export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; -export declare function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration; -export declare function createDefaultSetupConfiguration(): SetupConfiguration; -export type SetupConfigurationOverrides = { - features?: Partial; - agents?: Partial>>; - repository?: Partial; - ai?: Partial; - projects?: Partial; - createInitialTag?: boolean; - manageRepositoryVariables?: boolean; - manageRepositorySecrets?: boolean; - actionInputs?: Record; - storage?: { - secrets?: Partial; - variables?: Partial; - }; -}; -export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; -export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; -export declare function buildSetupPlan(configuration: SetupConfiguration): SetupPlan; -/** Builds the non-sensitive credential contract implied by the selected agents. */ -export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; -export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; -export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; -export declare function resolveSetupResourceScope(policy: SetupResourceStoragePolicy, name: string): SetupResourceScope; -export type SetupResourceKind = 'secret' | 'variable'; -export declare function getSetupResourceStoragePolicy(configuration: SetupConfiguration, kind: SetupResourceKind): SetupResourceStoragePolicy; -export declare function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration; -export declare function resolveSetupResourceTarget(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): SetupResourceTarget; -export declare function setupResourceExists(remote: SetupRemoteConfiguration | undefined, kind: SetupResourceKind, name: string): { - repository: boolean; - organization: boolean; - effective?: SetupResourceScope; -}; -export declare function shouldUpsertSetupResource(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): boolean; -export declare function validateSetupStorageAgainstRemote(configuration: SetupConfiguration, remote: SetupRemoteConfiguration): string[]; -export declare function usesOrganizationStorage(configuration: SetupConfiguration): boolean; +/** Public setup-policy boundary. Each concern is implemented in a focused policy module. */ +export * from './setup_configuration_defaults'; +export * from './setup_configuration_plan'; +export * from './setup_configuration_storage_policy'; +export * from './setup_configuration_validation'; diff --git a/build/cli/src/application/policies/setup_configuration_storage_policy.d.ts b/build/cli/src/application/policies/setup_configuration_storage_policy.d.ts new file mode 100644 index 00000000..293248aa --- /dev/null +++ b/build/cli/src/application/policies/setup_configuration_storage_policy.d.ts @@ -0,0 +1,15 @@ +import type { SetupConfiguration, SetupRemoteConfiguration, SetupResourceScope, SetupResourceStoragePolicy, SetupResourceTarget, SetupStorageConfiguration } from '../../domain/setup'; +export type SetupResourceKind = 'secret' | 'variable'; +export declare function resolveSetupResourceScope(policy: SetupResourceStoragePolicy, name: string): SetupResourceScope; +export declare function getSetupResourceStoragePolicy(configuration: SetupConfiguration, kind: SetupResourceKind): SetupResourceStoragePolicy; +export declare function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration; +export declare function resolveSetupResourceTarget(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): SetupResourceTarget; +export declare function setupResourceExists(remote: SetupRemoteConfiguration | undefined, kind: SetupResourceKind, name: string): { + repository: boolean; + organization: boolean; + effective?: SetupResourceScope; +}; +export declare function shouldUpsertSetupResource(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): boolean; +export declare function validateSetupStorageAgainstRemote(configuration: SetupConfiguration, remote: SetupRemoteConfiguration): string[]; +export declare function usesOrganizationStorage(configuration: SetupConfiguration): boolean; +export declare function validateStorageConfiguration(storage: SetupStorageConfiguration | undefined): string[]; diff --git a/build/cli/src/application/policies/setup_configuration_validation.d.ts b/build/cli/src/application/policies/setup_configuration_validation.d.ts new file mode 100644 index 00000000..5d74f652 --- /dev/null +++ b/build/cli/src/application/policies/setup_configuration_validation.d.ts @@ -0,0 +1,2 @@ +import type { SetupConfiguration } from '../../domain/setup'; +export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; diff --git a/build/cli/src/application/policies/workflow_queue_policy.d.ts b/build/cli/src/application/policies/workflow_queue_policy.d.ts index f69afed1..98937d72 100644 --- a/build/cli/src/application/policies/workflow_queue_policy.d.ts +++ b/build/cli/src/application/policies/workflow_queue_policy.d.ts @@ -3,7 +3,7 @@ * repository mutation queue. Keep these names aligned with workflow `name` * values in `.github/workflows` and the setup templates. */ -export declare const COPILOT_WORKFLOW_NAMES: readonly ["Copilot - Issue", "Copilot - Issue Comment", "Copilot - Commit", "Copilot - Pull Request", "Copilot - Pull Request Comment", "Task - Hotfix", "Task - Release"]; +export declare const COPILOT_WORKFLOW_NAMES: readonly ["Copilot - Issue", "Copilot - Issue Comment", "Copilot - Commit", "Copilot - Pull Request", "Copilot - Pull Request Comment", "Copilot - Close Inactive Issues", "Task - Hotfix", "Task - Release"]; export interface WorkflowPollingPolicy { maximumQueueWaitMilliseconds: number; initialDelayMilliseconds: number; diff --git a/build/cli/src/application/ports/issue_inactivity_ports.d.ts b/build/cli/src/application/ports/issue_inactivity_ports.d.ts new file mode 100644 index 00000000..e03f76c7 --- /dev/null +++ b/build/cli/src/application/ports/issue_inactivity_ports.d.ts @@ -0,0 +1,8 @@ +import type { IssueActivitySnapshot } from '../../domain/issue_inactivity'; +export interface IssueInactivityQueryPort { + listOpenIssuesByLabel(owner: string, repository: string, label: string, token: string): Promise; + getOpenIssue(owner: string, repository: string, issueNumber: number, token: string): Promise; +} +export interface IssueInactivityClockPort { + nowMilliseconds(): number; +} diff --git a/build/cli/src/application/usecases/actions/close_inactive_issues_use_case.d.ts b/build/cli/src/application/usecases/actions/close_inactive_issues_use_case.d.ts new file mode 100644 index 00000000..39e779fe --- /dev/null +++ b/build/cli/src/application/usecases/actions/close_inactive_issues_use_case.d.ts @@ -0,0 +1,14 @@ +import type { Execution } from '../../../data/model/execution'; +import type { Result } from '../../../data/model/result'; +import { ParamUseCase } from '../base/param_usecase'; +import type { IssueClosurePort } from '../../ports/issue_lifecycle_ports'; +import type { IssueInactivityClockPort, IssueInactivityQueryPort } from '../../ports/issue_inactivity_ports'; +/** Application boundary for the scheduled inactivity-maintenance action. */ +export declare class CloseInactiveIssuesUseCase implements ParamUseCase { + private readonly issueQueryPort; + private readonly issueClosurePort; + private readonly clock; + taskId: string; + constructor(issueQueryPort: IssueInactivityQueryPort, issueClosurePort: IssueClosurePort, clock: IssueInactivityClockPort); + invoke(param: Execution): Promise; +} diff --git a/build/cli/src/application/usecases/actions/close_inactive_issues_workflow.d.ts b/build/cli/src/application/usecases/actions/close_inactive_issues_workflow.d.ts new file mode 100644 index 00000000..d5ed6313 --- /dev/null +++ b/build/cli/src/application/usecases/actions/close_inactive_issues_workflow.d.ts @@ -0,0 +1,11 @@ +import type { Execution } from '../../../data/model/execution'; +import { Result } from '../../../data/model/result'; +import type { IssueClosurePort } from '../../ports/issue_lifecycle_ports'; +import type { IssueInactivityClockPort, IssueInactivityQueryPort } from '../../ports/issue_inactivity_ports'; +export interface CloseInactiveIssuesWorkflowDependencies { + readonly issueQueryPort: IssueInactivityQueryPort; + readonly issueClosurePort: IssueClosurePort; + readonly clock: IssueInactivityClockPort; +} +/** Scans waiting issues and closes only candidates that remain inactive. */ +export declare function runCloseInactiveIssuesWorkflow(param: Execution, dependencies: CloseInactiveIssuesWorkflowDependencies): Promise; diff --git a/build/cli/src/application/usecases/actions/initial_setup_request.d.ts b/build/cli/src/application/usecases/actions/initial_setup_request.d.ts new file mode 100644 index 00000000..615dd573 --- /dev/null +++ b/build/cli/src/application/usecases/actions/initial_setup_request.d.ts @@ -0,0 +1,14 @@ +import type { Execution } from '../../../data/model/execution'; +import type { IssueTypes } from '../../../data/model/issue_types'; +import type { Labels } from '../../../data/model/labels'; +import type { SetupConfiguration } from '../../../domain/setup'; +import type { SetupRepositoryContext } from './setup_resource_provisioning'; +/** Narrow input assembled by the execution adapter for the setup workflow. */ +export interface InitialSetupRequest extends SetupRepositoryContext { + labels: Labels; + issueTypes: IssueTypes; + setupConfiguration?: SetupConfiguration; + workflowUpdates: readonly string[]; +} +/** Converts the legacy execution aggregate into the setup use case's explicit request. */ +export declare function createInitialSetupRequest(execution: Execution): InitialSetupRequest; diff --git a/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts b/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts index 94717181..4e863414 100644 --- a/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts +++ b/build/cli/src/application/usecases/actions/initial_setup_workflow.d.ts @@ -1,12 +1,12 @@ -import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; import type { LatestTagQueryPort } from '../../ports/branch_tag_ports'; import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports'; import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports/repository_release_ports'; import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; -import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; -export interface InitialSetupWorkflowDependencies { +import type { SetupResourceProvisioningDependencies } from './setup_resource_provisioning'; +import type { InitialSetupRequest } from './initial_setup_request'; +export interface InitialSetupWorkflowDependencies extends SetupResourceProvisioningDependencies { authenticatedUserPort: AuthenticatedUserPort; initialLabelProvisioningPort: InitialLabelProvisioningPort; issueTypeProvisioningPort: IssueTypeProvisioningPort; @@ -14,9 +14,6 @@ export interface InitialSetupWorkflowDependencies { repositoryDefaultBranchPort: RepositoryDefaultBranchPort; repositoryTagPort: RepositoryTagPort; setupWorkspacePort: SetupWorkspacePort; - setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; - setupRepositorySecretsPort?: SetupRepositorySecretsPort; - setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; } /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ -export declare function runInitialSetupWorkflow(param: Execution, dependencies: InitialSetupWorkflowDependencies): Promise; +export declare function runInitialSetupWorkflow(request: InitialSetupRequest, dependencies: InitialSetupWorkflowDependencies): Promise; diff --git a/build/cli/src/application/usecases/actions/setup_resource_provisioning.d.ts b/build/cli/src/application/usecases/actions/setup_resource_provisioning.d.ts new file mode 100644 index 00000000..b484292b --- /dev/null +++ b/build/cli/src/application/usecases/actions/setup_resource_provisioning.d.ts @@ -0,0 +1,33 @@ +import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration, SetupResourceTarget } from '../../../domain/setup'; +import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +export interface SetupResourceProvisioningDependencies { + setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; + setupRepositorySecretsPort?: SetupRepositorySecretsPort; + setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; +} +export interface SetupRepositoryContext { + owner: string; + repo: string; + token: string; + setupCredentials?: SetupCredentialCollection; + setupRemoteConfiguration?: SetupRemoteConfiguration; +} +export type SetupResource = { + name: string; + value: string; +}; +export type SetupResourceGroup = { + target: SetupResourceTarget; + resources: SetupResource[]; +}; +export declare function ensureRepositoryVariables(context: SetupRepositoryContext, dependencies: SetupResourceProvisioningDependencies, setupConfiguration?: SetupConfiguration, remoteConfiguration?: SetupRemoteConfiguration): Promise<{ + step?: string; + errors: string[]; +}>; +export declare function ensureRepositorySecrets(context: SetupRepositoryContext, dependencies: SetupResourceProvisioningDependencies, setupConfiguration?: SetupConfiguration, remoteConfiguration?: SetupRemoteConfiguration): Promise<{ + step?: string; + errors: string[]; +}>; +export declare function resolveRemoteConfiguration(context: SetupRepositoryContext, dependencies: SetupResourceProvisioningDependencies, setupConfiguration: SetupConfiguration | undefined, errors: string[]): Promise; +/** Groups resources by their resolved storage target so each provider call is scoped explicitly. */ +export declare function groupSetupResources(resources: readonly SetupResource[], kind: 'secret' | 'variable', configuration: SetupConfiguration, remoteConfiguration?: SetupRemoteConfiguration): SetupResourceGroup[]; diff --git a/build/cli/src/application/usecases/single_action_use_case.d.ts b/build/cli/src/application/usecases/single_action_use_case.d.ts index 64806c8a..a2587dee 100644 --- a/build/cli/src/application/usecases/single_action_use_case.d.ts +++ b/build/cli/src/application/usecases/single_action_use_case.d.ts @@ -11,7 +11,8 @@ export declare class SingleActionUseCase implements ParamUseCase, publishGithubActionUseCase: ParamUseCase, createReleaseUseCase: ParamUseCase, createTagUseCase: ParamUseCase, thinkUseCase: ParamUseCase, initialSetupUseCase: ParamUseCase, checkProgressUseCase: ParamUseCase, detectPotentialProblemsUseCase: ParamUseCase, recommendStepsUseCase: ParamUseCase); + constructor(deployedActionUseCase: ParamUseCase, publishGithubActionUseCase: ParamUseCase, createReleaseUseCase: ParamUseCase, createTagUseCase: ParamUseCase, thinkUseCase: ParamUseCase, initialSetupUseCase: ParamUseCase, checkProgressUseCase: ParamUseCase, detectPotentialProblemsUseCase: ParamUseCase, recommendStepsUseCase: ParamUseCase, closeInactiveIssuesUseCase?: ParamUseCase | undefined); invoke(param: Execution): Promise; } diff --git a/build/cli/src/application/usecases/single_action_workflow.d.ts b/build/cli/src/application/usecases/single_action_workflow.d.ts index dceb16ae..46784b2d 100644 --- a/build/cli/src/application/usecases/single_action_workflow.d.ts +++ b/build/cli/src/application/usecases/single_action_workflow.d.ts @@ -11,5 +11,6 @@ export interface SingleActionWorkflowPorts { checkProgressUseCase: ParamUseCase; detectPotentialProblemsUseCase: ParamUseCase; recommendStepsUseCase: ParamUseCase; + closeInactiveIssuesUseCase?: ParamUseCase; } export declare function runSingleActionWorkflow(param: Execution, taskId: string, ports: SingleActionWorkflowPorts): Promise; diff --git a/build/cli/src/cli/cli_errors.d.ts b/build/cli/src/cli/cli_errors.d.ts new file mode 100644 index 00000000..66505353 --- /dev/null +++ b/build/cli/src/cli/cli_errors.d.ts @@ -0,0 +1,3 @@ +export declare const ERRORS: { + readonly GIT_REPOSITORY_NOT_FOUND: "❌ Git repository not found"; +}; diff --git a/build/cli/src/cli/setup_prompt_rendering.d.ts b/build/cli/src/cli/setup_prompt_rendering.d.ts new file mode 100644 index 00000000..2a683597 --- /dev/null +++ b/build/cli/src/cli/setup_prompt_rendering.d.ts @@ -0,0 +1,7 @@ +import type { DoctorCheckStatus, SetupCredentialCheck, SetupCredentialRequirement, SetupRemoteConfiguration, SetupVariable } from '../domain/setup'; +export declare function statusIcon(status: SetupCredentialCheck['status']): string; +export declare function doctorIcon(status: DoctorCheckStatus): string; +export declare function formatTask(task: string): string; +export declare function color(value: string, code: number): string; +export declare function renderBox(content: string, title: string, borderCode?: number): string; +export declare function renderRemoteConfiguration(remote: SetupRemoteConfiguration, variables: readonly SetupVariable[], requirements: readonly SetupCredentialRequirement[]): string; diff --git a/build/cli/src/data/model/action_types.d.ts b/build/cli/src/data/model/action_types.d.ts index 9fa3a23c..4b7560c5 100644 --- a/build/cli/src/data/model/action_types.d.ts +++ b/build/cli/src/data/model/action_types.d.ts @@ -9,4 +9,5 @@ export declare const ACTIONS: { readonly CHECK_PROGRESS: "check_progress_action"; readonly DETECT_POTENTIAL_PROBLEMS: "detect_potential_problems_action"; readonly RECOMMEND_STEPS: "recommend_steps_action"; + readonly CLOSE_INACTIVE_ISSUES: "close_inactive_issues_action"; }; diff --git a/build/cli/src/data/model/execution.d.ts b/build/cli/src/data/model/execution.d.ts index 062430d2..ab02613a 100644 --- a/build/cli/src/data/model/execution.d.ts +++ b/build/cli/src/data/model/execution.d.ts @@ -50,6 +50,7 @@ export declare class Execution { previousConfiguration: Config | undefined; currentConfiguration: Config; tokenUser: string | undefined; + inactivityThresholdHours: number; inputs: ExecutionInputs | undefined; get eventName(): string; get actor(): string; diff --git a/build/cli/src/data/model/execution_components.d.ts b/build/cli/src/data/model/execution_components.d.ts index 762767a5..dd57bcfc 100644 --- a/build/cli/src/data/model/execution_components.d.ts +++ b/build/cli/src/data/model/execution_components.d.ts @@ -38,5 +38,6 @@ export interface ExecutionComponents { projects: Projects; tokenUser?: string; welcome?: Welcome; + inactivityThresholdHours?: number; inputs?: ExecutionInputs; } diff --git a/build/cli/src/data/model/single_action.d.ts b/build/cli/src/data/model/single_action.d.ts index 5ec86825..435e142c 100644 --- a/build/cli/src/data/model/single_action.d.ts +++ b/build/cli/src/data/model/single_action.d.ts @@ -28,6 +28,7 @@ export declare class SingleAction { get isCheckProgressAction(): boolean; get isDetectPotentialProblemsAction(): boolean; get isRecommendStepsAction(): boolean; + get isCloseInactiveIssuesAction(): boolean; get enabledSingleAction(): boolean; get validSingleAction(): boolean; get isSingleActionWithoutIssue(): boolean; diff --git a/build/cli/src/data/repository/ai/agent_constants.d.ts b/build/cli/src/data/repository/ai/agent_constants.d.ts new file mode 100644 index 00000000..53d3fd52 --- /dev/null +++ b/build/cli/src/data/repository/ai/agent_constants.d.ts @@ -0,0 +1,2 @@ +/** Maximum time allowed for one external agent CLI request. */ +export declare const AGENT_REQUEST_TIMEOUT_MS = 900000; diff --git a/build/cli/src/data/repository/issue/issue_inactivity_repository.d.ts b/build/cli/src/data/repository/issue/issue_inactivity_repository.d.ts new file mode 100644 index 00000000..66057286 --- /dev/null +++ b/build/cli/src/data/repository/issue/issue_inactivity_repository.d.ts @@ -0,0 +1,11 @@ +import type { IssueInactivityQueryPort } from '../../../application/ports/issue_inactivity_ports'; +import type { IssueActivitySnapshot } from '../../../domain/issue_inactivity'; +import type { GithubClientPort } from '../../../infrastructure/github/ports/github_client_provider_port'; +import type { GithubIssueInactivityClient } from '../../../infrastructure/github/ports/github_issue_provider_ports'; +/** Reads the provider's issue activity timestamp and waiting-state labels. */ +export declare class IssueInactivityRepository implements IssueInactivityQueryPort { + private readonly githubClient; + constructor(githubClient: GithubClientPort); + listOpenIssuesByLabel: (owner: string, repository: string, label: string, token: string) => Promise; + getOpenIssue: (owner: string, repository: string, issueNumber: number, token: string) => Promise; +} diff --git a/build/cli/src/data/repository/workflow/workflow_status.d.ts b/build/cli/src/data/repository/workflow/workflow_status.d.ts new file mode 100644 index 00000000..7643a3dc --- /dev/null +++ b/build/cli/src/data/repository/workflow/workflow_status.d.ts @@ -0,0 +1,13 @@ +export declare const WORKFLOW_STATUS: { + readonly IN_PROGRESS: "in_progress"; + readonly QUEUED: "queued"; + readonly REQUESTED: "requested"; + readonly WAITING: "waiting"; + readonly PENDING: "pending"; + readonly COMPLETED: "completed"; + readonly FAILED: "failed"; + readonly CANCELLED: "cancelled"; + readonly SKIPPED: "skipped"; + readonly TIMED_OUT: "timed_out"; +}; +export declare const WORKFLOW_ACTIVE_STATUSES: readonly string[]; diff --git a/build/cli/src/domain/issue_inactivity.d.ts b/build/cli/src/domain/issue_inactivity.d.ts new file mode 100644 index 00000000..0c985716 --- /dev/null +++ b/build/cli/src/domain/issue_inactivity.d.ts @@ -0,0 +1,30 @@ +/** Default inactivity window used by the scheduled issue-maintenance action. */ +export declare const DEFAULT_INACTIVITY_THRESHOLD_HOURS = 168; +/** Maximum supported window (one year) for a finite, operationally useful value. */ +export declare const MAX_INACTIVITY_THRESHOLD_HOURS = 8760; +export interface IssueActivitySnapshot { + readonly number: number; + readonly updatedAt?: string; + readonly isPullRequest: boolean; + readonly labels: readonly string[]; +} +export type IssueInactivityDecision = { + readonly kind: 'close'; + readonly inactiveForMilliseconds: number; +} | { + readonly kind: 'skip'; + readonly reason: 'pull-request' | 'not-waiting' | 'agent-processing' | 'missing-activity-timestamp' | 'future-activity' | 'recent-activity' | 'invalid-threshold'; +}; +export interface IssueInactivityEvaluationInput { + readonly issue: IssueActivitySnapshot; + readonly waitingLabels: readonly string[]; + readonly agentActivityLabel: string; + readonly thresholdHours: number; + readonly nowMilliseconds: number; +} +/** + * Decides whether an issue can be closed without depending on GitHub or time + * APIs. GitHub's `updated_at` is treated as the last activity observed by the + * provider; this includes comments and issue metadata changes. + */ +export declare function evaluateIssueInactivity(input: IssueInactivityEvaluationInput): IssueInactivityDecision; diff --git a/build/cli/src/domain/setup.d.ts b/build/cli/src/domain/setup.d.ts index 12b58f46..04095388 100644 --- a/build/cli/src/domain/setup.d.ts +++ b/build/cli/src/domain/setup.d.ts @@ -1,6 +1,6 @@ import type { AgentProvider, AgentTask } from './agent'; import type { PullRequestDescriptionMode } from './pull_request_description'; -export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'issueTemplates' | 'pullRequestTemplate'; +export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'inactiveIssueClosure' | 'issueTemplates' | 'pullRequestTemplate'; export interface SetupFeatures { [feature: string]: boolean; } @@ -25,6 +25,7 @@ export interface SetupRepositoryConfiguration { desiredAssigneesCount: number; desiredReviewersCount: number; mergeTimeout: number; + inactivityThresholdHours: number; issueLocale: string; pullRequestLocale: string; commitPrefixTransforms: string; diff --git a/build/cli/src/infrastructure/composition/github_issue_client_factory.d.ts b/build/cli/src/infrastructure/composition/github_issue_client_factory.d.ts index 8b2e3f91..587f1901 100644 --- a/build/cli/src/infrastructure/composition/github_issue_client_factory.d.ts +++ b/build/cli/src/infrastructure/composition/github_issue_client_factory.d.ts @@ -1,8 +1,9 @@ -import { OctokitIssueAssignmentClientAdapter, OctokitIssueContentClientAdapter, OctokitIssueLabelProvisioningClientAdapter, OctokitIssueLabelsClientAdapter, OctokitIssueLifecycleClientAdapter, OctokitIssueMetadataClientAdapter, OctokitIssueTitleClientAdapter } from "../github/octokit_issue_adapters"; +import { OctokitIssueAssignmentClientAdapter, OctokitIssueContentClientAdapter, OctokitIssueInactivityClientAdapter, OctokitIssueLabelProvisioningClientAdapter, OctokitIssueLabelsClientAdapter, OctokitIssueLifecycleClientAdapter, OctokitIssueMetadataClientAdapter, OctokitIssueTitleClientAdapter } from "../github/octokit_issue_adapters"; export declare const createIssueAssignmentClient: () => OctokitIssueAssignmentClientAdapter; export declare const createIssueContentClient: () => OctokitIssueContentClientAdapter; export declare const createIssueLabelProvisioningClient: () => OctokitIssueLabelProvisioningClientAdapter; export declare const createIssueLabelsClient: () => OctokitIssueLabelsClientAdapter; export declare const createIssueLifecycleClient: () => OctokitIssueLifecycleClientAdapter; +export declare const createIssueInactivityClient: () => OctokitIssueInactivityClientAdapter; export declare const createIssueMetadataClient: () => OctokitIssueMetadataClientAdapter; export declare const createIssueTitleClient: () => OctokitIssueTitleClientAdapter; diff --git a/build/cli/src/infrastructure/composition/issue_inactivity_composition_root.d.ts b/build/cli/src/infrastructure/composition/issue_inactivity_composition_root.d.ts new file mode 100644 index 00000000..8f1c0bfb --- /dev/null +++ b/build/cli/src/infrastructure/composition/issue_inactivity_composition_root.d.ts @@ -0,0 +1,2 @@ +import { CloseInactiveIssuesUseCase } from '../../application/usecases/actions/close_inactive_issues_use_case'; +export declare function createCloseInactiveIssuesUseCase(): CloseInactiveIssuesUseCase; diff --git a/build/cli/src/infrastructure/github/octokit_issue_adapters.d.ts b/build/cli/src/infrastructure/github/octokit_issue_adapters.d.ts index 511b9fbd..7ad95b6b 100644 --- a/build/cli/src/infrastructure/github/octokit_issue_adapters.d.ts +++ b/build/cli/src/infrastructure/github/octokit_issue_adapters.d.ts @@ -1,5 +1,5 @@ import type { GithubClientPort } from "./ports/github_client_provider_port"; -import type { GithubIssueAssignmentClient, GithubIssueContentClient, GithubIssueLabelsClient, GithubIssueLifecycleClient, GithubIssueMetadataClient, GithubIssueTitleClient } from "./ports/github_issue_provider_ports"; +import type { GithubIssueAssignmentClient, GithubIssueContentClient, GithubIssueInactivityClient, GithubIssueLabelsClient, GithubIssueLifecycleClient, GithubIssueMetadataClient, GithubIssueTitleClient } from "./ports/github_issue_provider_ports"; import type { GithubIssueLabelProvisioningClient } from "./ports/github_issue_label_provisioning_protocol"; export declare class OctokitIssueAssignmentClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueAssignmentClient; @@ -16,6 +16,9 @@ export declare class OctokitIssueLabelsClientAdapter implements GithubClientPort export declare class OctokitIssueLifecycleClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueLifecycleClient; } +export declare class OctokitIssueInactivityClientAdapter implements GithubClientPort { + getClient(token: string): GithubIssueInactivityClient; +} export declare class OctokitIssueMetadataClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueMetadataClient; } diff --git a/build/cli/src/infrastructure/github/ports/github_issue_provider_ports.d.ts b/build/cli/src/infrastructure/github/ports/github_issue_provider_ports.d.ts index 2a5929a9..12d52591 100644 --- a/build/cli/src/infrastructure/github/ports/github_issue_provider_ports.d.ts +++ b/build/cli/src/infrastructure/github/ports/github_issue_provider_ports.d.ts @@ -10,6 +10,34 @@ export interface GithubIssueLifecycleClient { }; }; } +export interface GithubIssueInactivityClient { + paginate: { + iterator(method: (parameters: Record) => Promise<{ + data: GithubIssueActivity[]; + }>, parameters: Record): AsyncIterable<{ + data: GithubIssueActivity[]; + }>; + }; + rest: { + issues: { + listForRepo(parameters: Record): Promise<{ + data: GithubIssueActivity[]; + }>; + get(parameters: Record): Promise<{ + data: GithubIssueActivity; + }>; + }; + }; +} +export interface GithubIssueActivity { + number: number; + updated_at?: string | null; + state?: 'open' | 'closed' | string; + pull_request?: unknown; + labels?: Array<{ + name?: string; + } | string>; +} export interface GithubIssueContentClient { paginate: { iterator(method: (parameters: Record) => Promise<{ diff --git a/build/cli/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts b/build/cli/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts new file mode 100644 index 00000000..b2b8e31c --- /dev/null +++ b/build/cli/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts @@ -0,0 +1,4 @@ +import type { IssueInactivityClockPort } from '../../application/ports/issue_inactivity_ports'; +export declare class SystemIssueInactivityClockAdapter implements IssueInactivityClockPort { + nowMilliseconds(): number; +} diff --git a/build/github_action/index.js b/build/github_action/index.js index 5cecd150..ad64dc6f 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -50392,15 +50392,15 @@ function buildAgentTasks(values, environment = process.env) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildAgentTasksFromInputs = buildAgentTasksFromInputs; exports.buildAgentTasksFromValues = buildAgentTasksFromValues; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const agent_configuration_builder_1 = __nccwpck_require__(81248); const agent_1 = __nccwpck_require__(89040); function buildAgentTasksFromInputs(read) { - const provider = read(constants_1.INPUT_KEYS.AGENT_PROVIDER)?.trim() || agent_1.DEFAULT_AGENT_PROVIDER; - const modelProvider = read(constants_1.INPUT_KEYS.AGENT_MODEL_PROVIDER)?.trim() || agent_1.DEFAULT_MODEL_PROVIDER; - const model = read(constants_1.INPUT_KEYS.AGENT_MODEL)?.trim() || agent_1.DEFAULT_AGENT_MODEL; - const effort = read(constants_1.INPUT_KEYS.AGENT_EFFORT) ?? ''; - const command = read(constants_1.INPUT_KEYS.AGENT_COMMAND) ?? ''; + const provider = read(input_keys_1.INPUT_KEYS.AGENT_PROVIDER)?.trim() || agent_1.DEFAULT_AGENT_PROVIDER; + const modelProvider = read(input_keys_1.INPUT_KEYS.AGENT_MODEL_PROVIDER)?.trim() || agent_1.DEFAULT_MODEL_PROVIDER; + const model = read(input_keys_1.INPUT_KEYS.AGENT_MODEL)?.trim() || agent_1.DEFAULT_AGENT_MODEL; + const effort = read(input_keys_1.INPUT_KEYS.AGENT_EFFORT) ?? ''; + const command = read(input_keys_1.INPUT_KEYS.AGENT_COMMAND) ?? ''; const role = (name) => ({ provider: read(`${name}-provider`), modelProvider: read(`${name}-model-provider`), @@ -50415,18 +50415,18 @@ function buildAgentTasksFromInputs(read) { effort, command, findings: { - provider: read(constants_1.INPUT_KEYS.FINDINGS_PROVIDER), - modelProvider: read(constants_1.INPUT_KEYS.FINDINGS_MODEL_PROVIDER), - model: read(constants_1.INPUT_KEYS.FINDINGS_MODEL), - effort: read(constants_1.INPUT_KEYS.FINDINGS_EFFORT), - command: read(constants_1.INPUT_KEYS.FINDINGS_COMMAND), + provider: read(input_keys_1.INPUT_KEYS.FINDINGS_PROVIDER), + modelProvider: read(input_keys_1.INPUT_KEYS.FINDINGS_MODEL_PROVIDER), + model: read(input_keys_1.INPUT_KEYS.FINDINGS_MODEL), + effort: read(input_keys_1.INPUT_KEYS.FINDINGS_EFFORT), + command: read(input_keys_1.INPUT_KEYS.FINDINGS_COMMAND), }, fixer: { - provider: read(constants_1.INPUT_KEYS.FIXER_PROVIDER), - modelProvider: read(constants_1.INPUT_KEYS.FIXER_MODEL_PROVIDER), - model: read(constants_1.INPUT_KEYS.FIXER_MODEL), - effort: read(constants_1.INPUT_KEYS.FIXER_EFFORT), - command: read(constants_1.INPUT_KEYS.FIXER_COMMAND), + provider: read(input_keys_1.INPUT_KEYS.FIXER_PROVIDER), + modelProvider: read(input_keys_1.INPUT_KEYS.FIXER_MODEL_PROVIDER), + model: read(input_keys_1.INPUT_KEYS.FIXER_MODEL), + effort: read(input_keys_1.INPUT_KEYS.FIXER_EFFORT), + command: read(input_keys_1.INPUT_KEYS.FIXER_COMMAND), }, planner: role('planner'), reviewer: role('reviewer'), @@ -50587,6 +50587,192 @@ function buildImages(values) { } +/***/ }), + +/***/ 14387: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_IMAGE_CONFIG = void 0; +/** Default illustration URLs used when an action does not receive custom images. */ +exports.DEFAULT_IMAGE_CONFIG = { + issue: { + automatic: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp" + ], + feature: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", + ], + bugfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" + ], + hotfix: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" + ], + release: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", + ], + docs: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", + ], + chore: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", + ], + }, + pullRequest: { + automatic: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + ], + feature: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", + ], + bugfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", + ], + hotfix: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", + ], + release: [ + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", + ], + docs: [ + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", + ], + chore: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", + ], + }, + commit: { + automatic: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + feature: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + bugfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + hotfix: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + release: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + docs: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ], + chore: [ + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", + "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", + ] + } +}; + + /***/ }), /***/ 20236: @@ -50658,7 +50844,7 @@ const github_action_execution_1 = __nccwpck_require__(39691); const github_event_inputs_1 = __nccwpck_require__(63452); const common_action_1 = __nccwpck_require__(42238); const main_run_lifecycle_1 = __nccwpck_require__(916); -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const logger_1 = __nccwpck_require__(91151); const github_execution_admission_composition_root_1 = __nccwpck_require__(54954); const lifecycle_state_composition_root_1 = __nccwpck_require__(4673); @@ -50666,7 +50852,7 @@ const copilot_evidence_composition_root_1 = __nccwpck_require__(64686); const github_action_summary_composition_root_1 = __nccwpck_require__(75305); const agent_activity_composition_root_1 = __nccwpck_require__(94253); async function runGitHubAction() { - if ((0, input_boolean_policy_1.isEnabledInput)((0, github_action_input_1.getGithubActionInput)(constants_1.INPUT_KEYS.QUEUE_GATE_ONLY))) { + if ((0, input_boolean_policy_1.isEnabledInput)((0, github_action_input_1.getGithubActionInput)(input_keys_1.INPUT_KEYS.QUEUE_GATE_ONLY))) { await runQueueGateOnly(); return; } @@ -50677,11 +50863,11 @@ async function runGitHubAction() { repo: github.context.repo, }); (0, logger_1.logInfo)('GitHub Action: runGitHubAction started.'); - const debug = (0, input_boolean_policy_1.isEnabledInput)((0, github_action_input_1.getGithubActionInput)(constants_1.INPUT_KEYS.DEBUG)); + const debug = (0, input_boolean_policy_1.isEnabledInput)((0, github_action_input_1.getGithubActionInput)(input_keys_1.INPUT_KEYS.DEBUG)); if (debug) { (0, logger_1.logInfo)('Debug mode is enabled. Full logs will be included in the report.'); } - const token = (0, github_action_input_1.getGithubActionInput)(constants_1.INPUT_KEYS.TOKEN, { required: true }); + const token = (0, github_action_input_1.getGithubActionInput)(input_keys_1.INPUT_KEYS.TOKEN, { required: true }); const singleAction = (0, github_action_execution_1.readGithubActionSingleAction)(github_action_input_1.getGithubActionInput); const admission = await (0, github_execution_admission_composition_root_1.createGithubExecutionAdmissionUseCase)().invoke({ actor: eventInputs.actor, @@ -50717,7 +50903,7 @@ async function runQueueGateOnly() { actor: github.context.actor, repo: github.context.repo, }); - const token = (0, github_action_input_1.getGithubActionInput)(constants_1.INPUT_KEYS.TOKEN, { required: true }); + const token = (0, github_action_input_1.getGithubActionInput)(input_keys_1.INPUT_KEYS.TOKEN, { required: true }); await (0, main_run_lifecycle_1.waitForPreviousWorkflowRuns)(token, eventInputs.repo); } catch { @@ -50747,7 +50933,8 @@ if (typeof process.env.JEST_WORKER_ID === 'undefined') { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionAgentTasks = readGithubActionAgentTasks; exports.readGithubActionAiInputs = readGithubActionAiInputs; -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); +const input_keys_1 = __nccwpck_require__(88539); const input_boolean_policy_1 = __nccwpck_require__(18330); const input_number_policy_1 = __nccwpck_require__(47165); const input_values_policy_1 = __nccwpck_require__(68841); @@ -50758,8 +50945,8 @@ function readGithubActionAgentTasks(getInput, _configurationSource) { } function readGithubActionAiInputs(getInput) { const requestedAgentTasks = (0, agent_input_builder_1.buildAgentTasksFromInputs)(getInput); - const pullRequestDescription = (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); - const verifyCommands = getInput(constants_1.INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) + const pullRequestDescription = (0, input_boolean_policy_1.isEnabledInput)(getInput(input_keys_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION)); + const verifyCommands = getInput(input_keys_1.INPUT_KEYS.BUGBOT_FIX_VERIFY_COMMANDS) .split(',') .map((command) => command.trim()) .filter((command) => command.length > 0); @@ -50767,13 +50954,13 @@ function readGithubActionAiInputs(getInput) { requestedAgentTasks, pullRequestDescription, pullRequestDescriptionMode: pullRequestDescription - ? (0, pull_request_description_1.normalizePullRequestDescriptionMode)(getInput(constants_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) + ? (0, pull_request_description_1.normalizePullRequestDescriptionMode)(getInput(input_keys_1.INPUT_KEYS.AI_PULL_REQUEST_DESCRIPTION_MODE)) : 'disabled', - membersOnly: (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.AI_MEMBERS_ONLY)), - includeReasoning: (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.AI_INCLUDE_REASONING)), - ignoreFiles: (0, input_values_policy_1.parseDelimitedValues)(getInput(constants_1.INPUT_KEYS.AI_IGNORE_FILES)), - bugbotSeverity: getInput(constants_1.INPUT_KEYS.BUGBOT_SEVERITY) || constants_1.BUGBOT_MIN_SEVERITY, - bugbotCommentLimit: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(getInput(constants_1.INPUT_KEYS.BUGBOT_COMMENT_LIMIT), constants_1.BUGBOT_MAX_COMMENTS, 200), + membersOnly: (0, input_boolean_policy_1.isEnabledInput)(getInput(input_keys_1.INPUT_KEYS.AI_MEMBERS_ONLY)), + includeReasoning: (0, input_boolean_policy_1.isEnabledInput)(getInput(input_keys_1.INPUT_KEYS.AI_INCLUDE_REASONING)), + ignoreFiles: (0, input_values_policy_1.parseDelimitedValues)(getInput(input_keys_1.INPUT_KEYS.AI_IGNORE_FILES)), + bugbotSeverity: getInput(input_keys_1.INPUT_KEYS.BUGBOT_SEVERITY) || bugbot_constants_1.BUGBOT_MIN_SEVERITY, + bugbotCommentLimit: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(getInput(input_keys_1.INPUT_KEYS.BUGBOT_COMMENT_LIMIT), bugbot_constants_1.BUGBOT_MAX_COMMENTS, 200), bugbotFixVerifyCommands: verifyCommands, }; } @@ -50788,19 +50975,19 @@ function readGithubActionAiInputs(getInput) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionBranchInputs = readGithubActionBranchInputs; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); function readGithubActionBranchInputs(getInput) { - const main = getInput(constants_1.INPUT_KEYS.MAIN_BRANCH); + const main = getInput(input_keys_1.INPUT_KEYS.MAIN_BRANCH); return { main, defaultBranch: main, - development: getInput(constants_1.INPUT_KEYS.DEVELOPMENT_BRANCH), - featureTree: getInput(constants_1.INPUT_KEYS.FEATURE_TREE), - bugfixTree: getInput(constants_1.INPUT_KEYS.BUGFIX_TREE), - hotfixTree: getInput(constants_1.INPUT_KEYS.HOTFIX_TREE), - releaseTree: getInput(constants_1.INPUT_KEYS.RELEASE_TREE), - docsTree: getInput(constants_1.INPUT_KEYS.DOCS_TREE), - choreTree: getInput(constants_1.INPUT_KEYS.CHORE_TREE), + development: getInput(input_keys_1.INPUT_KEYS.DEVELOPMENT_BRANCH), + featureTree: getInput(input_keys_1.INPUT_KEYS.FEATURE_TREE), + bugfixTree: getInput(input_keys_1.INPUT_KEYS.BUGFIX_TREE), + hotfixTree: getInput(input_keys_1.INPUT_KEYS.HOTFIX_TREE), + releaseTree: getInput(input_keys_1.INPUT_KEYS.RELEASE_TREE), + docsTree: getInput(input_keys_1.INPUT_KEYS.DOCS_TREE), + choreTree: getInput(input_keys_1.INPUT_KEYS.CHORE_TREE), }; } @@ -50957,7 +51144,7 @@ const ai_1 = __nccwpck_require__(37478); const hotfix_1 = __nccwpck_require__(18537); const release_1 = __nccwpck_require__(74715); const single_action_1 = __nccwpck_require__(45898); -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const input_boolean_policy_1 = __nccwpck_require__(18330); const input_number_policy_1 = __nccwpck_require__(47165); const input_values_policy_1 = __nccwpck_require__(68841); @@ -50976,11 +51163,14 @@ const github_action_project_inputs_1 = __nccwpck_require__(1293); const execution_builder_1 = __nccwpck_require__(20236); const configuration_builders_1 = __nccwpck_require__(19094); const project_details_loader_1 = __nccwpck_require__(73448); +const issue_inactivity_1 = __nccwpck_require__(38572); async function buildGithubActionExecution(input) { const { getInput, eventInputs, projectQuery, debug, singleAction, token } = input; const aiInputs = (0, github_action_ai_inputs_1.readGithubActionAiInputs)(getInput); - (0, github_action_runtime_1.prepareGithubAgentRuntime)(aiInputs.requestedAgentTasks); - const projects = await (0, project_details_loader_1.loadProjectDetails)(projectQuery, (0, input_values_policy_1.parseDelimitedValues)(getInput(constants_1.INPUT_KEYS.PROJECT_IDS)), eventInputs.repo.owner, token); + if (!singleAction.isCloseInactiveIssuesAction) { + (0, github_action_runtime_1.prepareGithubAgentRuntime)(aiInputs.requestedAgentTasks); + } + const projects = await (0, project_details_loader_1.loadProjectDetails)(projectQuery, (0, input_values_policy_1.parseDelimitedValues)(getInput(input_keys_1.INPUT_KEYS.PROJECT_IDS)), eventInputs.repo.owner, token); const projectInputs = (0, github_action_project_inputs_1.readGithubActionProjectInputs)(getInput, projects); const imageConfiguration = (0, github_action_image_inputs_1.readGithubActionImageInputs)(getInput); const workflowInputs = (0, github_action_workflow_inputs_1.readGithubActionWorkflowInputs)(getInput); @@ -50991,11 +51181,12 @@ async function buildGithubActionExecution(input) { const branchInputs = (0, github_action_branch_inputs_1.readGithubActionBranchInputs)(getInput); return (0, execution_builder_1.buildExecution)({ debug, + inactivityThresholdHours: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(getInput(input_keys_1.INPUT_KEYS.INACTIVITY_THRESHOLD_HOURS), issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS, issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS), singleAction, commitPrefixBuilder: getCommitPrefixBuilder(getInput), - issue: (0, configuration_builders_1.buildIssue)((0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.BRANCH_MANAGEMENT_ALWAYS)), (0, input_boolean_policy_1.isEnabledInput)(getInput(constants_1.INPUT_KEYS.REOPEN_ISSUE_ON_PUSH)), (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.DESIRED_ASSIGNEES_COUNT), 0), eventInputs), - pullRequest: (0, configuration_builders_1.buildPullRequest)((0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), (0, input_number_policy_1.parseNonNegativeIntegerInput)(getInput(constants_1.INPUT_KEYS.PULL_REQUEST_MERGE_TIMEOUT), 0), eventInputs), - emoji: (0, configuration_builders_1.buildEmoji)(getInput(constants_1.INPUT_KEYS.EMOJI_LABELED_TITLE) === 'true', getInput(constants_1.INPUT_KEYS.BRANCH_MANAGEMENT_EMOJI)), + issue: (0, configuration_builders_1.buildIssue)((0, input_boolean_policy_1.isEnabledInput)(getInput(input_keys_1.INPUT_KEYS.BRANCH_MANAGEMENT_ALWAYS)), (0, input_boolean_policy_1.isEnabledInput)(getInput(input_keys_1.INPUT_KEYS.REOPEN_ISSUE_ON_PUSH)), (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.DESIRED_ASSIGNEES_COUNT), 0), eventInputs), + pullRequest: (0, configuration_builders_1.buildPullRequest)((0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), (0, input_number_policy_1.parseNonNegativeIntegerInput)(getInput(input_keys_1.INPUT_KEYS.PULL_REQUEST_MERGE_TIMEOUT), 0), eventInputs), + emoji: (0, configuration_builders_1.buildEmoji)(getInput(input_keys_1.INPUT_KEYS.EMOJI_LABELED_TITLE) === 'true', getInput(input_keys_1.INPUT_KEYS.BRANCH_MANAGEMENT_EMOJI)), images: (0, configuration_builders_1.buildImages)(imageConfiguration), tokens: (0, configuration_builders_1.buildTokens)(token), ai: new ai_1.Ai('', aiInputs.requestedAgentTasks.findings.model, aiInputs.pullRequestDescription, aiInputs.membersOnly, aiInputs.ignoreFiles, aiInputs.includeReasoning, aiInputs.bugbotSeverity, aiInputs.bugbotCommentLimit, aiInputs.bugbotFixVerifyCommands, aiInputs.requestedAgentTasks, aiInputs.pullRequestDescriptionMode), @@ -51013,10 +51204,10 @@ async function buildGithubActionExecution(input) { }); } function readGithubActionSingleAction(getInput) { - return new single_action_1.SingleAction(getInput(constants_1.INPUT_KEYS.SINGLE_ACTION), getInput(constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE), getInput(constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION), getInput(constants_1.INPUT_KEYS.SINGLE_ACTION_TITLE), getInput(constants_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG)); + return new single_action_1.SingleAction(getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_TITLE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG)); } function getCommitPrefixBuilder(getInput) { - return getInput(constants_1.INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash'; + return getInput(input_keys_1.INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash'; } @@ -51104,21 +51295,21 @@ function getGithubActionInput(key, options) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionIssueTypeInputs = readGithubActionIssueTypeInputs; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); function readIssueType(getInput, name, description, color) { return { name: getInput(name), description: getInput(description), color: getInput(color) }; } function readGithubActionIssueTypeInputs(getInput) { return { - task: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_TASK, constants_1.INPUT_KEYS.ISSUE_TYPE_TASK_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_TASK_COLOR), - bug: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_BUG, constants_1.INPUT_KEYS.ISSUE_TYPE_BUG_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_BUG_COLOR), - feature: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_FEATURE, constants_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_COLOR), - documentation: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION, constants_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_COLOR), - maintenance: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE, constants_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_COLOR), - hotfix: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX, constants_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_COLOR), - release: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_RELEASE, constants_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_COLOR), - question: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_QUESTION, constants_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_COLOR), - help: readIssueType(getInput, constants_1.INPUT_KEYS.ISSUE_TYPE_HELP, constants_1.INPUT_KEYS.ISSUE_TYPE_HELP_DESCRIPTION, constants_1.INPUT_KEYS.ISSUE_TYPE_HELP_COLOR), + task: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_TASK, input_keys_1.INPUT_KEYS.ISSUE_TYPE_TASK_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_TASK_COLOR), + bug: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_BUG, input_keys_1.INPUT_KEYS.ISSUE_TYPE_BUG_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_BUG_COLOR), + feature: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_FEATURE, input_keys_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_FEATURE_COLOR), + documentation: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_DOCUMENTATION_COLOR), + maintenance: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE, input_keys_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_MAINTENANCE_COLOR), + hotfix: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HOTFIX_COLOR), + release: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_RELEASE, input_keys_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_RELEASE_COLOR), + question: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_QUESTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_QUESTION_COLOR), + help: readIssueType(getInput, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HELP, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HELP_DESCRIPTION, input_keys_1.INPUT_KEYS.ISSUE_TYPE_HELP_COLOR), }; } @@ -51132,39 +51323,39 @@ function readGithubActionIssueTypeInputs(getInput) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionLabelInputs = readGithubActionLabelInputs; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); function readGithubActionLabelInputs(getInput) { return { - branching: { launcher: getInput(constants_1.INPUT_KEYS.BRANCH_MANAGEMENT_LAUNCHER_LABEL) }, + branching: { launcher: getInput(input_keys_1.INPUT_KEYS.BRANCH_MANAGEMENT_LAUNCHER_LABEL) }, workflow: { - bug: getInput(constants_1.INPUT_KEYS.BUG_LABEL), bugfix: getInput(constants_1.INPUT_KEYS.BUGFIX_LABEL), - hotfix: getInput(constants_1.INPUT_KEYS.HOTFIX_LABEL), enhancement: getInput(constants_1.INPUT_KEYS.ENHANCEMENT_LABEL), - feature: getInput(constants_1.INPUT_KEYS.FEATURE_LABEL), release: getInput(constants_1.INPUT_KEYS.RELEASE_LABEL), - question: getInput(constants_1.INPUT_KEYS.QUESTION_LABEL), help: getInput(constants_1.INPUT_KEYS.HELP_LABEL), - deploy: getInput(constants_1.INPUT_KEYS.DEPLOY_LABEL), deployed: getInput(constants_1.INPUT_KEYS.DEPLOYED_LABEL), - docs: getInput(constants_1.INPUT_KEYS.DOCS_LABEL), documentation: getInput(constants_1.INPUT_KEYS.DOCUMENTATION_LABEL), - chore: getInput(constants_1.INPUT_KEYS.CHORE_LABEL), maintenance: getInput(constants_1.INPUT_KEYS.MAINTENANCE_LABEL), + bug: getInput(input_keys_1.INPUT_KEYS.BUG_LABEL), bugfix: getInput(input_keys_1.INPUT_KEYS.BUGFIX_LABEL), + hotfix: getInput(input_keys_1.INPUT_KEYS.HOTFIX_LABEL), enhancement: getInput(input_keys_1.INPUT_KEYS.ENHANCEMENT_LABEL), + feature: getInput(input_keys_1.INPUT_KEYS.FEATURE_LABEL), release: getInput(input_keys_1.INPUT_KEYS.RELEASE_LABEL), + question: getInput(input_keys_1.INPUT_KEYS.QUESTION_LABEL), help: getInput(input_keys_1.INPUT_KEYS.HELP_LABEL), + deploy: getInput(input_keys_1.INPUT_KEYS.DEPLOY_LABEL), deployed: getInput(input_keys_1.INPUT_KEYS.DEPLOYED_LABEL), + docs: getInput(input_keys_1.INPUT_KEYS.DOCS_LABEL), documentation: getInput(input_keys_1.INPUT_KEYS.DOCUMENTATION_LABEL), + chore: getInput(input_keys_1.INPUT_KEYS.CHORE_LABEL), maintenance: getInput(input_keys_1.INPUT_KEYS.MAINTENANCE_LABEL), }, priorities: { - high: getInput(constants_1.INPUT_KEYS.PRIORITY_HIGH_LABEL), medium: getInput(constants_1.INPUT_KEYS.PRIORITY_MEDIUM_LABEL), - low: getInput(constants_1.INPUT_KEYS.PRIORITY_LOW_LABEL), none: getInput(constants_1.INPUT_KEYS.PRIORITY_NONE_LABEL), + high: getInput(input_keys_1.INPUT_KEYS.PRIORITY_HIGH_LABEL), medium: getInput(input_keys_1.INPUT_KEYS.PRIORITY_MEDIUM_LABEL), + low: getInput(input_keys_1.INPUT_KEYS.PRIORITY_LOW_LABEL), none: getInput(input_keys_1.INPUT_KEYS.PRIORITY_NONE_LABEL), }, sizes: { - xxl: getInput(constants_1.INPUT_KEYS.SIZE_XXL_LABEL), xl: getInput(constants_1.INPUT_KEYS.SIZE_XL_LABEL), - l: getInput(constants_1.INPUT_KEYS.SIZE_L_LABEL), m: getInput(constants_1.INPUT_KEYS.SIZE_M_LABEL), - s: getInput(constants_1.INPUT_KEYS.SIZE_S_LABEL), xs: getInput(constants_1.INPUT_KEYS.SIZE_XS_LABEL), + xxl: getInput(input_keys_1.INPUT_KEYS.SIZE_XXL_LABEL), xl: getInput(input_keys_1.INPUT_KEYS.SIZE_XL_LABEL), + l: getInput(input_keys_1.INPUT_KEYS.SIZE_L_LABEL), m: getInput(input_keys_1.INPUT_KEYS.SIZE_M_LABEL), + s: getInput(input_keys_1.INPUT_KEYS.SIZE_S_LABEL), xs: getInput(input_keys_1.INPUT_KEYS.SIZE_XS_LABEL), }, lifecycle: { - aiProcessing: getInput(constants_1.INPUT_KEYS.STATE_AI_PROCESSING_LABEL), - planned: getInput(constants_1.INPUT_KEYS.STATE_PLANNED_LABEL), - inProgress: getInput(constants_1.INPUT_KEYS.STATE_IN_PROGRESS_LABEL), - reviewing: getInput(constants_1.INPUT_KEYS.STATE_REVIEWING_LABEL), - changesRequested: getInput(constants_1.INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), - verified: getInput(constants_1.INPUT_KEYS.STATE_VERIFIED_LABEL), - ready: getInput(constants_1.INPUT_KEYS.STATE_READY_LABEL), - blocked: getInput(constants_1.INPUT_KEYS.STATE_BLOCKED_LABEL), - awaitingMaintainer: getInput(constants_1.INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), - awaitingIssueAuthor: getInput(constants_1.INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), + aiProcessing: getInput(input_keys_1.INPUT_KEYS.STATE_AI_PROCESSING_LABEL), + planned: getInput(input_keys_1.INPUT_KEYS.STATE_PLANNED_LABEL), + inProgress: getInput(input_keys_1.INPUT_KEYS.STATE_IN_PROGRESS_LABEL), + reviewing: getInput(input_keys_1.INPUT_KEYS.STATE_REVIEWING_LABEL), + changesRequested: getInput(input_keys_1.INPUT_KEYS.STATE_CHANGES_REQUESTED_LABEL), + verified: getInput(input_keys_1.INPUT_KEYS.STATE_VERIFIED_LABEL), + ready: getInput(input_keys_1.INPUT_KEYS.STATE_READY_LABEL), + blocked: getInput(input_keys_1.INPUT_KEYS.STATE_BLOCKED_LABEL), + awaitingMaintainer: getInput(input_keys_1.INPUT_KEYS.STATE_AWAITING_MAINTAINER_LABEL), + awaitingIssueAuthor: getInput(input_keys_1.INPUT_KEYS.STATE_AWAITING_ISSUE_AUTHOR_LABEL), }, }; } @@ -51180,11 +51371,11 @@ function readGithubActionLabelInputs(getInput) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionLocaleInputs = readGithubActionLocaleInputs; const locale_1 = __nccwpck_require__(9832); -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); function readGithubActionLocaleInputs(getInput) { return { - issue: getInput(constants_1.INPUT_KEYS.ISSUES_LOCALE) || locale_1.Locale.DEFAULT, - pullRequest: getInput(constants_1.INPUT_KEYS.PULL_REQUESTS_LOCALE) || locale_1.Locale.DEFAULT, + issue: getInput(input_keys_1.INPUT_KEYS.ISSUES_LOCALE) || locale_1.Locale.DEFAULT, + pullRequest: getInput(input_keys_1.INPUT_KEYS.PULL_REQUESTS_LOCALE) || locale_1.Locale.DEFAULT, }; } @@ -51198,14 +51389,14 @@ function readGithubActionLocaleInputs(getInput) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionProjectInputs = readGithubActionProjectInputs; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); function readGithubActionProjectInputs(getInput, projects) { return { projects, - issueCreated: getInput(constants_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_CREATED), - pullRequestCreated: getInput(constants_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_CREATED), - issueInProgress: getInput(constants_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_IN_PROGRESS), - pullRequestInProgress: getInput(constants_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS), + issueCreated: getInput(input_keys_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_CREATED), + pullRequestCreated: getInput(input_keys_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_CREATED), + issueInProgress: getInput(input_keys_1.INPUT_KEYS.PROJECT_COLUMN_ISSUE_IN_PROGRESS), + pullRequestInProgress: getInput(input_keys_1.INPUT_KEYS.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS), }; } @@ -51264,39 +51455,39 @@ function uniqueAgentConfigurations(agentTasks) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionThresholdInputs = readGithubActionThresholdInputs; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const input_number_policy_1 = __nccwpck_require__(47165); function readGithubActionThresholdInputs(getInput) { return { xxl: { - lines: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_LINES), 1000), - files: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_FILES), 20), - commits: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_COMMITS), 10), + lines: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_LINES), 1000), + files: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_FILES), 20), + commits: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XXL_THRESHOLD_COMMITS), 10), }, xl: { - lines: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XL_THRESHOLD_LINES), 500), - files: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XL_THRESHOLD_FILES), 10), - commits: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XL_THRESHOLD_COMMITS), 5), + lines: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XL_THRESHOLD_LINES), 500), + files: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XL_THRESHOLD_FILES), 10), + commits: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XL_THRESHOLD_COMMITS), 5), }, l: { - lines: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_L_THRESHOLD_LINES), 250), - files: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_L_THRESHOLD_FILES), 5), - commits: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_L_THRESHOLD_COMMITS), 3), + lines: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_L_THRESHOLD_LINES), 250), + files: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_L_THRESHOLD_FILES), 5), + commits: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_L_THRESHOLD_COMMITS), 3), }, m: { - lines: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_M_THRESHOLD_LINES), 100), - files: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_M_THRESHOLD_FILES), 3), - commits: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_M_THRESHOLD_COMMITS), 2), + lines: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_M_THRESHOLD_LINES), 100), + files: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_M_THRESHOLD_FILES), 3), + commits: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_M_THRESHOLD_COMMITS), 2), }, s: { - lines: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_S_THRESHOLD_LINES), 50), - files: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_S_THRESHOLD_FILES), 2), - commits: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_S_THRESHOLD_COMMITS), 1), + lines: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_S_THRESHOLD_LINES), 50), + files: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_S_THRESHOLD_FILES), 2), + commits: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_S_THRESHOLD_COMMITS), 1), }, xs: { - lines: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XS_THRESHOLD_LINES), 25), - files: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XS_THRESHOLD_FILES), 1), - commits: (0, input_number_policy_1.parseIntegerInput)(getInput(constants_1.INPUT_KEYS.SIZE_XS_THRESHOLD_COMMITS), 1), + lines: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XS_THRESHOLD_LINES), 25), + files: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XS_THRESHOLD_FILES), 1), + commits: (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.SIZE_XS_THRESHOLD_COMMITS), 1), }, }; } @@ -51311,11 +51502,11 @@ function readGithubActionThresholdInputs(getInput) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readGithubActionWorkflowInputs = readGithubActionWorkflowInputs; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); function readGithubActionWorkflowInputs(getInput) { return { - release: getInput(constants_1.INPUT_KEYS.RELEASE_WORKFLOW), - hotfix: getInput(constants_1.INPUT_KEYS.HOTFIX_WORKFLOW), + release: getInput(input_keys_1.INPUT_KEYS.RELEASE_WORKFLOW), + hotfix: getInput(input_keys_1.INPUT_KEYS.HOTFIX_WORKFLOW), }; } @@ -51359,36 +51550,37 @@ function requireNonEmptyContextValue(value, label) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildImageConfiguration = buildImageConfiguration; -const constants_1 = __nccwpck_require__(15415); +const default_image_config_1 = __nccwpck_require__(14387); +const input_keys_1 = __nccwpck_require__(88539); const input_boolean_policy_1 = __nccwpck_require__(18330); const input_values_policy_1 = __nccwpck_require__(68841); const imageInputKeys = { issue: { - automatic: constants_1.INPUT_KEYS.IMAGES_ISSUE_AUTOMATIC, - feature: constants_1.INPUT_KEYS.IMAGES_ISSUE_FEATURE, - bugfix: constants_1.INPUT_KEYS.IMAGES_ISSUE_BUGFIX, - release: constants_1.INPUT_KEYS.IMAGES_ISSUE_RELEASE, - hotfix: constants_1.INPUT_KEYS.IMAGES_ISSUE_HOTFIX, - docs: constants_1.INPUT_KEYS.IMAGES_ISSUE_DOCS, - chore: constants_1.INPUT_KEYS.IMAGES_ISSUE_CHORE, + automatic: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_AUTOMATIC, + feature: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_FEATURE, + bugfix: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_BUGFIX, + release: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_RELEASE, + hotfix: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_HOTFIX, + docs: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_DOCS, + chore: input_keys_1.INPUT_KEYS.IMAGES_ISSUE_CHORE, }, pullRequest: { - automatic: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_AUTOMATIC, - feature: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_FEATURE, - bugfix: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_BUGFIX, - release: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_RELEASE, - hotfix: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_HOTFIX, - docs: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_DOCS, - chore: constants_1.INPUT_KEYS.IMAGES_PULL_REQUEST_CHORE, + automatic: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_AUTOMATIC, + feature: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_FEATURE, + bugfix: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_BUGFIX, + release: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_RELEASE, + hotfix: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_HOTFIX, + docs: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_DOCS, + chore: input_keys_1.INPUT_KEYS.IMAGES_PULL_REQUEST_CHORE, }, commit: { - automatic: constants_1.INPUT_KEYS.IMAGES_COMMIT_AUTOMATIC, - feature: constants_1.INPUT_KEYS.IMAGES_COMMIT_FEATURE, - bugfix: constants_1.INPUT_KEYS.IMAGES_COMMIT_BUGFIX, - release: constants_1.INPUT_KEYS.IMAGES_COMMIT_RELEASE, - hotfix: constants_1.INPUT_KEYS.IMAGES_COMMIT_HOTFIX, - docs: constants_1.INPUT_KEYS.IMAGES_COMMIT_DOCS, - chore: constants_1.INPUT_KEYS.IMAGES_COMMIT_CHORE, + automatic: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_AUTOMATIC, + feature: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_FEATURE, + bugfix: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_BUGFIX, + release: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_RELEASE, + hotfix: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_HOTFIX, + docs: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_DOCS, + chore: input_keys_1.INPUT_KEYS.IMAGES_COMMIT_CHORE, }, }; function buildImageConfiguration(read) { @@ -51399,14 +51591,14 @@ function buildImageConfiguration(read) { const configured = (0, input_values_policy_1.parseDelimitedValues)(read(imageInputKeys[group][variant])); variants[variant] = configured.length > 0 ? configured - : [...constants_1.DEFAULT_IMAGE_CONFIG[group][variant]]; + : [...default_image_config_1.DEFAULT_IMAGE_CONFIG[group][variant]]; } groups[group] = variants; } return { - onIssue: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.IMAGES_ON_ISSUE)), - onPullRequest: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.IMAGES_ON_PULL_REQUEST)), - onCommit: (0, input_boolean_policy_1.isEnabledInput)(read(constants_1.INPUT_KEYS.IMAGES_ON_COMMIT)), + onIssue: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.IMAGES_ON_ISSUE)), + onPullRequest: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.IMAGES_ON_PULL_REQUEST)), + onCommit: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.IMAGES_ON_COMMIT)), ...groups, }; } @@ -51571,7 +51763,7 @@ exports.runMainRoute = runMainRoute; const core = __importStar(__nccwpck_require__(81078)); const chalk_1 = __importDefault(__nccwpck_require__(8578)); const boxen_1 = __importDefault(__nccwpck_require__(11652)); -const constants_1 = __nccwpck_require__(15415); +const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); const main_run_dispatcher_1 = __nccwpck_require__(28586); const workflow_context_1 = __nccwpck_require__(55224); @@ -51626,7 +51818,7 @@ function logWelcomeMessage(execution) { margin: 1, borderStyle: 'round', borderColor: 'cyan', - title: constants_1.TITLE, + title: product_identity_1.TITLE, titleAlignment: 'center', })); } @@ -51797,149 +51989,378 @@ function resolveWorkflowIdentifier(workflowRef) { /***/ }), -/***/ 75999: +/***/ 88539: /***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = void 0; -exports.toApplicationError = toApplicationError; -/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ -class ApplicationError extends Error { - constructor(message, kind = 'unknown', options = {}) { - super(message); - this.name = 'ApplicationError'; - this.kind = kind; - this.retryable = options.retryable ?? false; - this.cause = options.cause; - } -} -exports.ApplicationError = ApplicationError; -function toApplicationError(error, message, kind = 'unknown', options = {}) { - return error instanceof ApplicationError - ? error - : new ApplicationError(message, kind, { ...options, cause: error }); -} - - -/***/ }), - -/***/ 72995: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.buildActionSummary = buildActionSummary; -const result_1 = __nccwpck_require__(73817); -const github_comment_publication_policy_1 = __nccwpck_require__(72712); -/** Builds a bounded, publication-safe GitHub Actions Job Summary. */ -function buildActionSummary(context) { - const failures = context.results.filter(result => !result.success && result.executed); - const findingStates = context.results - .map(result => getFindingStateCounts(result.payload)) - .find(Boolean); - const hasActionableFindings = (findingStates?.open ?? 0) + (findingStates?.reopened ?? 0) > 0; - const status = failures.length === 0 && !hasActionableFindings ? '✅ Success' : '❌ Failure'; - const target = context.pullRequestNumber > 0 - ? `PR #${context.pullRequestNumber}` - : context.issueNumber > 0 - ? `Issue #${context.issueNumber}` - : 'Repository run'; - const lifecycle = context.lifecycleState ? `\`${(0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(context.lifecycleState, 100)}\`` : '—'; - const rows = [ - `| Status | ${status} |`, - `| Event | \`${escapeTable(context.eventName)}\` |`, - `| Target | ${escapeTable(target)} |`, - `| Lifecycle | ${lifecycle} |`, - `| PR description policy | ${escapeTable(context.pullRequestDescriptionMode ?? '—')} |`, - `| Results | ${context.results.length} |`, - `| Finding states | ${formatFindingStates(findingStates)} |`, - ]; - return [ - '# Copilot execution', - '', - `Repository: [${escapeTable(`${context.owner}/${context.repository}`)}](https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)})`, - '', - '| Property | Value |', - '| --- | --- |', - ...rows, - '', - '## Result details', - '', - renderResults(context.results), - '', - ].join('\n'); -} -function getFindingStateCounts(value) { - const payload = (0, result_1.getResultPayload)(value); - const stateCounts = (0, result_1.getResultPayload)(payload?.findingStates); - if (!stateCounts) - return undefined; - const states = ['open', 'reopened', 'fixed', 'obsolete', 'dismissed']; - if (!states.every(state => typeof stateCounts[state] === 'number')) - return undefined; - return Object.fromEntries(states.map(state => [state, stateCounts[state]])); -} -function formatFindingStates(counts) { - if (!counts) - return '—'; - return Object.entries(counts) - .filter(([, value]) => value > 0) - .map(([state, value]) => `${state}=${value}`) - .join(', ') || 'none'; -} -function renderResults(results) { - if (results.length === 0) - return '_No application result was produced._'; - return results.map(result => { - const icon = result.success ? '✅' : '❌'; - const details = result.steps - .filter(step => step.trim()) - .map(step => ` - ${(0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(step, 1000)}`); - const errors = result.errors - .map(error => ` - **Error:** ${(0, github_comment_publication_policy_1.sanitizePublishedError)(error.message)}`); - return [`- ${icon} **${escapeTable(result.id || 'Unnamed result')}**`, ...details, ...errors].join('\n'); - }).join('\n'); -} -function escapeTable(value) { - return String(value ?? '').replace(/[|\r\n]/g, match => match === '|' ? '\\|' : ' '); -} +exports.INPUT_KEYS = void 0; +/** Canonical action and CLI input vocabulary shared by input mappers. */ +exports.INPUT_KEYS = { + // Debug + DEBUG: 'debug', + // Welcome + WELCOME_TITLE: 'welcome-title', + WELCOME_MESSAGES: 'welcome-messages', + // Single action + SINGLE_ACTION: 'single-action', + SINGLE_ACTION_ISSUE: 'single-action-issue', + SINGLE_ACTION_VERSION: 'single-action-version', + SINGLE_ACTION_TITLE: 'single-action-title', + SINGLE_ACTION_CHANGELOG: 'single-action-changelog', + INACTIVITY_THRESHOLD_HOURS: 'inactivity-threshold-hours', + // Tokens + TOKEN: 'token', + QUEUE_GATE_ONLY: 'queue-gate-only', + // Agent selection + AGENT_PROVIDER: 'agent-provider', + AGENT_MODEL_PROVIDER: 'agent-model-provider', + AGENT_EFFORT: 'agent-effort', + AGENT_MODEL: 'agent-model', + AGENT_COMMAND: 'agent-command', + FINDINGS_PROVIDER: 'findings-provider', + FINDINGS_MODEL_PROVIDER: 'findings-model-provider', + FINDINGS_EFFORT: 'findings-effort', + FINDINGS_MODEL: 'findings-model', + FINDINGS_COMMAND: 'findings-command', + FIXER_PROVIDER: 'fixer-provider', + FIXER_MODEL_PROVIDER: 'fixer-model-provider', + FIXER_EFFORT: 'fixer-effort', + FIXER_MODEL: 'fixer-model', + FIXER_COMMAND: 'fixer-command', + PLANNER_PROVIDER: 'planner-provider', + PLANNER_MODEL_PROVIDER: 'planner-model-provider', + PLANNER_EFFORT: 'planner-effort', + PLANNER_MODEL: 'planner-model', + PLANNER_COMMAND: 'planner-command', + REVIEWER_PROVIDER: 'reviewer-provider', + REVIEWER_MODEL_PROVIDER: 'reviewer-model-provider', + REVIEWER_EFFORT: 'reviewer-effort', + REVIEWER_MODEL: 'reviewer-model', + REVIEWER_COMMAND: 'reviewer-command', + TESTER_PROVIDER: 'tester-provider', + TESTER_MODEL_PROVIDER: 'tester-model-provider', + TESTER_EFFORT: 'tester-effort', + TESTER_MODEL: 'tester-model', + TESTER_COMMAND: 'tester-command', + RELEASE_PROVIDER: 'release-provider', + RELEASE_MODEL_PROVIDER: 'release-model-provider', + RELEASE_EFFORT: 'release-effort', + RELEASE_MODEL: 'release-model', + RELEASE_COMMAND: 'release-command', + // AI configuration + AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', + AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', + AI_MEMBERS_ONLY: 'ai-members-only', + AI_IGNORE_FILES: 'ai-ignore-files', + AI_INCLUDE_REASONING: 'ai-include-reasoning', + BUGBOT_SEVERITY: 'bugbot-severity', + BUGBOT_COMMENT_LIMIT: 'bugbot-comment-limit', + BUGBOT_FIX_VERIFY_COMMANDS: 'bugbot-fix-verify-commands', + // Projects + PROJECT_IDS: 'project-ids', + PROJECT_COLUMN_ISSUE_CREATED: 'project-column-issue-created', + PROJECT_COLUMN_PULL_REQUEST_CREATED: 'project-column-pull-request-created', + PROJECT_COLUMN_ISSUE_IN_PROGRESS: 'project-column-issue-in-progress', + PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: 'project-column-pull-request-in-progress', + // Images + IMAGES_ON_ISSUE: 'images-on-issue', + IMAGES_ON_PULL_REQUEST: 'images-on-pull-request', + IMAGES_ON_COMMIT: 'images-on-commit', + IMAGES_ISSUE_AUTOMATIC: 'images-issue-automatic', + IMAGES_ISSUE_FEATURE: 'images-issue-feature', + IMAGES_ISSUE_BUGFIX: 'images-issue-bugfix', + IMAGES_ISSUE_DOCS: 'images-issue-docs', + IMAGES_ISSUE_CHORE: 'images-issue-chore', + IMAGES_ISSUE_RELEASE: 'images-issue-release', + IMAGES_ISSUE_HOTFIX: 'images-issue-hotfix', + IMAGES_PULL_REQUEST_AUTOMATIC: 'images-pull-request-automatic', + IMAGES_PULL_REQUEST_FEATURE: 'images-pull-request-feature', + IMAGES_PULL_REQUEST_BUGFIX: 'images-pull-request-bugfix', + IMAGES_PULL_REQUEST_RELEASE: 'images-pull-request-release', + IMAGES_PULL_REQUEST_HOTFIX: 'images-pull-request-hotfix', + IMAGES_PULL_REQUEST_DOCS: 'images-pull-request-docs', + IMAGES_PULL_REQUEST_CHORE: 'images-pull-request-chore', + IMAGES_COMMIT_AUTOMATIC: 'images-commit-automatic', + IMAGES_COMMIT_FEATURE: 'images-commit-feature', + IMAGES_COMMIT_BUGFIX: 'images-commit-bugfix', + IMAGES_COMMIT_RELEASE: 'images-commit-release', + IMAGES_COMMIT_HOTFIX: 'images-commit-hotfix', + IMAGES_COMMIT_DOCS: 'images-commit-docs', + IMAGES_COMMIT_CHORE: 'images-commit-chore', + // Workflows + RELEASE_WORKFLOW: 'release-workflow', + HOTFIX_WORKFLOW: 'hotfix-workflow', + // Emoji + EMOJI_LABELED_TITLE: 'emoji-labeled-title', + BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', + // Labels + BRANCH_MANAGEMENT_LAUNCHER_LABEL: 'branch-management-launcher-label', + BUGFIX_LABEL: 'bugfix-label', + BUG_LABEL: 'bug-label', + HOTFIX_LABEL: 'hotfix-label', + ENHANCEMENT_LABEL: 'enhancement-label', + FEATURE_LABEL: 'feature-label', + RELEASE_LABEL: 'release-label', + QUESTION_LABEL: 'question-label', + HELP_LABEL: 'help-label', + DEPLOY_LABEL: 'deploy-label', + DEPLOYED_LABEL: 'deployed-label', + DOCS_LABEL: 'docs-label', + DOCUMENTATION_LABEL: 'documentation-label', + CHORE_LABEL: 'chore-label', + MAINTENANCE_LABEL: 'maintenance-label', + PRIORITY_HIGH_LABEL: 'priority-high-label', + PRIORITY_MEDIUM_LABEL: 'priority-medium-label', + PRIORITY_LOW_LABEL: 'priority-low-label', + PRIORITY_NONE_LABEL: 'priority-none-label', + SIZE_XXL_LABEL: 'size-xxl-label', + SIZE_XL_LABEL: 'size-xl-label', + SIZE_L_LABEL: 'size-l-label', + SIZE_M_LABEL: 'size-m-label', + SIZE_S_LABEL: 'size-s-label', + SIZE_XS_LABEL: 'size-xs-label', + // Lifecycle label inputs + STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', + STATE_PLANNED_LABEL: 'state-planned-label', + STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', + STATE_REVIEWING_LABEL: 'state-reviewing-label', + STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', + STATE_VERIFIED_LABEL: 'state-verified-label', + STATE_READY_LABEL: 'state-ready-label', + STATE_BLOCKED_LABEL: 'state-blocked-label', + STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', + STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', + // Issue Types + ISSUE_TYPE_BUG: 'issue-type-bug', + ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', + ISSUE_TYPE_BUG_COLOR: 'issue-type-bug-color', + ISSUE_TYPE_HOTFIX: 'issue-type-hotfix', + ISSUE_TYPE_HOTFIX_DESCRIPTION: 'issue-type-hotfix-description', + ISSUE_TYPE_HOTFIX_COLOR: 'issue-type-hotfix-color', + ISSUE_TYPE_FEATURE: 'issue-type-feature', + ISSUE_TYPE_FEATURE_DESCRIPTION: 'issue-type-feature-description', + ISSUE_TYPE_FEATURE_COLOR: 'issue-type-feature-color', + ISSUE_TYPE_DOCUMENTATION: 'issue-type-documentation', + ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: 'issue-type-documentation-description', + ISSUE_TYPE_DOCUMENTATION_COLOR: 'issue-type-documentation-color', + ISSUE_TYPE_MAINTENANCE: 'issue-type-maintenance', + ISSUE_TYPE_MAINTENANCE_DESCRIPTION: 'issue-type-maintenance-description', + ISSUE_TYPE_MAINTENANCE_COLOR: 'issue-type-maintenance-color', + ISSUE_TYPE_RELEASE: 'issue-type-release', + ISSUE_TYPE_RELEASE_DESCRIPTION: 'issue-type-release-description', + ISSUE_TYPE_RELEASE_COLOR: 'issue-type-release-color', + ISSUE_TYPE_QUESTION: 'issue-type-question', + ISSUE_TYPE_QUESTION_DESCRIPTION: 'issue-type-question-description', + ISSUE_TYPE_QUESTION_COLOR: 'issue-type-question-color', + ISSUE_TYPE_HELP: 'issue-type-help', + ISSUE_TYPE_HELP_DESCRIPTION: 'issue-type-help-description', + ISSUE_TYPE_HELP_COLOR: 'issue-type-help-color', + ISSUE_TYPE_TASK: 'issue-type-task', + ISSUE_TYPE_TASK_DESCRIPTION: 'issue-type-task-description', + ISSUE_TYPE_TASK_COLOR: 'issue-type-task-color', + // Locale + ISSUES_LOCALE: 'issues-locale', + PULL_REQUESTS_LOCALE: 'pull-requests-locale', + // Size Thresholds + SIZE_XXL_THRESHOLD_LINES: 'size-xxl-threshold-lines', + SIZE_XXL_THRESHOLD_FILES: 'size-xxl-threshold-files', + SIZE_XXL_THRESHOLD_COMMITS: 'size-xxl-threshold-commits', + SIZE_XL_THRESHOLD_LINES: 'size-xl-threshold-lines', + SIZE_XL_THRESHOLD_FILES: 'size-xl-threshold-files', + SIZE_XL_THRESHOLD_COMMITS: 'size-xl-threshold-commits', + SIZE_L_THRESHOLD_LINES: 'size-l-threshold-lines', + SIZE_L_THRESHOLD_FILES: 'size-l-threshold-files', + SIZE_L_THRESHOLD_COMMITS: 'size-l-threshold-commits', + SIZE_M_THRESHOLD_LINES: 'size-m-threshold-lines', + SIZE_M_THRESHOLD_FILES: 'size-m-threshold-files', + SIZE_M_THRESHOLD_COMMITS: 'size-m-threshold-commits', + SIZE_S_THRESHOLD_LINES: 'size-s-threshold-lines', + SIZE_S_THRESHOLD_FILES: 'size-s-threshold-files', + SIZE_S_THRESHOLD_COMMITS: 'size-s-threshold-commits', + SIZE_XS_THRESHOLD_LINES: 'size-xs-threshold-lines', + SIZE_XS_THRESHOLD_FILES: 'size-xs-threshold-files', + SIZE_XS_THRESHOLD_COMMITS: 'size-xs-threshold-commits', + // Branches + MAIN_BRANCH: 'main-branch', + DEVELOPMENT_BRANCH: 'development-branch', + FEATURE_TREE: 'feature-tree', + BUGFIX_TREE: 'bugfix-tree', + HOTFIX_TREE: 'hotfix-tree', + RELEASE_TREE: 'release-tree', + DOCS_TREE: 'docs-tree', + CHORE_TREE: 'chore-tree', + // Commit + COMMIT_PREFIX_TRANSFORMS: 'commit-prefix-transforms', + // Issue + BRANCH_MANAGEMENT_ALWAYS: 'branch-management-always', + REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', + DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', + // Pull Request + PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', + PULL_REQUEST_DESIRED_REVIEWERS_COUNT: 'desired-reviewers-count', + PULL_REQUEST_MERGE_TIMEOUT: 'merge-timeout', +}; /***/ }), -/***/ 79966: +/***/ 18739: /***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.replaceAgentActivityLabel = replaceAgentActivityLabel; -/** Adds or removes one activity label without touching unrelated labels. */ -function replaceAgentActivityLabel(currentLabels, activityLabel, active) { - const normalizedActivityLabel = activityLabel.trim().toLowerCase(); - if (!normalizedActivityLabel) - return [...currentLabels]; - const retained = currentLabels.filter(label => label.trim().toLowerCase() !== normalizedActivityLabel); - return active ? [...retained, activityLabel] : retained; -} +exports.TITLE = void 0; +exports.TITLE = 'Copilot'; /***/ }), -/***/ 15375: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ 75999: +/***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.shouldTrackAgentActivity = shouldTrackAgentActivity; -const agent_1 = __nccwpck_require__(89040); -/** Decides whether a route can invoke an agent for its current event. */ -function shouldTrackAgentActivity(execution, route) { - if (!hasTarget(execution)) +exports.ApplicationError = void 0; +exports.toApplicationError = toApplicationError; +/** Semantic error contract: safe to publish, while the original cause stays available to diagnostics. */ +class ApplicationError extends Error { + constructor(message, kind = 'unknown', options = {}) { + super(message); + this.name = 'ApplicationError'; + this.kind = kind; + this.retryable = options.retryable ?? false; + this.cause = options.cause; + } +} +exports.ApplicationError = ApplicationError; +function toApplicationError(error, message, kind = 'unknown', options = {}) { + return error instanceof ApplicationError + ? error + : new ApplicationError(message, kind, { ...options, cause: error }); +} + + +/***/ }), + +/***/ 72995: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildActionSummary = buildActionSummary; +const result_1 = __nccwpck_require__(73817); +const github_comment_publication_policy_1 = __nccwpck_require__(72712); +/** Builds a bounded, publication-safe GitHub Actions Job Summary. */ +function buildActionSummary(context) { + const failures = context.results.filter(result => !result.success && result.executed); + const findingStates = context.results + .map(result => getFindingStateCounts(result.payload)) + .find(Boolean); + const hasActionableFindings = (findingStates?.open ?? 0) + (findingStates?.reopened ?? 0) > 0; + const status = failures.length === 0 && !hasActionableFindings ? '✅ Success' : '❌ Failure'; + const target = context.pullRequestNumber > 0 + ? `PR #${context.pullRequestNumber}` + : context.issueNumber > 0 + ? `Issue #${context.issueNumber}` + : 'Repository run'; + const lifecycle = context.lifecycleState ? `\`${(0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(context.lifecycleState, 100)}\`` : '—'; + const rows = [ + `| Status | ${status} |`, + `| Event | \`${escapeTable(context.eventName)}\` |`, + `| Target | ${escapeTable(target)} |`, + `| Lifecycle | ${lifecycle} |`, + `| PR description policy | ${escapeTable(context.pullRequestDescriptionMode ?? '—')} |`, + `| Results | ${context.results.length} |`, + `| Finding states | ${formatFindingStates(findingStates)} |`, + ]; + return [ + '# Copilot execution', + '', + `Repository: [${escapeTable(`${context.owner}/${context.repository}`)}](https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)})`, + '', + '| Property | Value |', + '| --- | --- |', + ...rows, + '', + '## Result details', + '', + renderResults(context.results), + '', + ].join('\n'); +} +function getFindingStateCounts(value) { + const payload = (0, result_1.getResultPayload)(value); + const stateCounts = (0, result_1.getResultPayload)(payload?.findingStates); + if (!stateCounts) + return undefined; + const states = ['open', 'reopened', 'fixed', 'obsolete', 'dismissed']; + if (!states.every(state => typeof stateCounts[state] === 'number')) + return undefined; + return Object.fromEntries(states.map(state => [state, stateCounts[state]])); +} +function formatFindingStates(counts) { + if (!counts) + return '—'; + return Object.entries(counts) + .filter(([, value]) => value > 0) + .map(([state, value]) => `${state}=${value}`) + .join(', ') || 'none'; +} +function renderResults(results) { + if (results.length === 0) + return '_No application result was produced._'; + return results.map(result => { + const icon = result.success ? '✅' : '❌'; + const details = result.steps + .filter(step => step.trim()) + .map(step => ` - ${(0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(step, 1000)}`); + const errors = result.errors + .map(error => ` - **Error:** ${(0, github_comment_publication_policy_1.sanitizePublishedError)(error.message)}`); + return [`- ${icon} **${escapeTable(result.id || 'Unnamed result')}**`, ...details, ...errors].join('\n'); + }).join('\n'); +} +function escapeTable(value) { + return String(value ?? '').replace(/[|\r\n]/g, match => match === '|' ? '\\|' : ' '); +} + + +/***/ }), + +/***/ 79966: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.replaceAgentActivityLabel = replaceAgentActivityLabel; +/** Adds or removes one activity label without touching unrelated labels. */ +function replaceAgentActivityLabel(currentLabels, activityLabel, active) { + const normalizedActivityLabel = activityLabel.trim().toLowerCase(); + if (!normalizedActivityLabel) + return [...currentLabels]; + const retained = currentLabels.filter(label => label.trim().toLowerCase() !== normalizedActivityLabel); + return active ? [...retained, activityLabel] : retained; +} + + +/***/ }), + +/***/ 15375: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.shouldTrackAgentActivity = shouldTrackAgentActivity; +const agent_1 = __nccwpck_require__(89040); +/** Decides whether a route can invoke an agent for its current event. */ +function shouldTrackAgentActivity(execution, route) { + if (!hasTarget(execution)) return false; switch (route) { case 'issue': @@ -52465,6 +52886,23 @@ function findPreviousIssueBranch(branches, issueNumber, branchTypes) { } +/***/ }), + +/***/ 51389: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.BUGBOT_MIN_SEVERITY = exports.BUGBOT_MAX_COMMENTS = exports.BUGBOT_MARKER_PREFIX = void 0; +/** Hidden marker prefix used to reconcile Bugbot findings across comments. */ +exports.BUGBOT_MARKER_PREFIX = 'copilot-bugbot'; +/** Maximum number of individual Bugbot comments published for one analysis. */ +exports.BUGBOT_MAX_COMMENTS = 20; +/** Minimum severity published by default. */ +exports.BUGBOT_MIN_SEVERITY = 'low'; + + /***/ }), /***/ 53822: @@ -53313,7 +53751,7 @@ const MAX_DEBUG_LOG_LENGTH = 12000; /** Resolves the GitHub discussion that receives a result comment. */ function resolveResultPublicationIssueNumber(input) { if (input.isSingleAction) - return input.singleActionIssue; + return input.singleActionIssue > 0 ? input.singleActionIssue : undefined; if (input.isIssue) return input.issueNumber; if (input.isPullRequest) @@ -53507,7 +53945,7 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun /***/ }), -/***/ 56637: +/***/ 23381: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -53517,22 +53955,8 @@ exports.SETUP_FEATURE_DESCRIPTIONS = exports.SETUP_AGENT_TASKS = void 0; exports.createDefaultSetupStorageConfiguration = createDefaultSetupStorageConfiguration; exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; exports.mergeSetupConfiguration = mergeSetupConfiguration; -exports.validateSetupConfiguration = validateSetupConfiguration; -exports.buildSetupPlan = buildSetupPlan; -exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; -exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; -exports.buildSetupActionInputs = buildSetupActionInputs; -exports.resolveSetupResourceScope = resolveSetupResourceScope; -exports.getSetupResourceStoragePolicy = getSetupResourceStoragePolicy; -exports.getSetupStorageConfiguration = getSetupStorageConfiguration; -exports.resolveSetupResourceTarget = resolveSetupResourceTarget; -exports.setupResourceExists = setupResourceExists; -exports.shouldUpsertSetupResource = shouldUpsertSetupResource; -exports.validateSetupStorageAgainstRemote = validateSetupStorageAgainstRemote; -exports.usesOrganizationStorage = usesOrganizationStorage; const agent_1 = __nccwpck_require__(89040); -const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); -const pull_request_description_1 = __nccwpck_require__(45315); +const issue_inactivity_1 = __nccwpck_require__(38572); exports.SETUP_AGENT_TASKS = [ 'planner', 'findings', @@ -53551,37 +53975,10 @@ exports.SETUP_FEATURE_DESCRIPTIONS = { hotfix: 'Hotfix workflow: emergency release from a production tag', agentProvisioning: 'Agent CLI provisioning check workflow', credentialHealth: 'Read-only remote credential health workflow for setup and doctor', + inactiveIssueClosure: 'Close issues after inactivity while waiting for an issuer or issue author', issueTemplates: 'Issue templates for feature, bug, documentation, and operations', pullRequestTemplate: 'Pull request template', }; -const WORKFLOW_FILES = { - issues: ['copilot_issue.yml'], - pullRequests: ['copilot_pull_request.yml'], - commits: ['copilot_commit.yml'], - issueComments: ['copilot_issue_comment.yml'], - pullRequestComments: ['copilot_pull_request_comment.yml'], - release: ['release_workflow.yml'], - hotfix: ['hotfix_workflow.yml'], - agentProvisioning: ['agent-cli-provisioning.yml'], - credentialHealth: ['copilot_credential_health.yml'], -}; -const ISSUE_TEMPLATE_FILES = [ - 'config.yml', - 'feature_request.yml', - 'bug_report.yml', - 'doc_update.yml', - 'chore_task.yml', - 'help_request.yml', - 'hotfix.yml', - 'release.yml', -]; -const SECRET_BY_MODEL_PROVIDER = { - openai: 'OPENAI_API_KEY', - anthropic: 'ANTHROPIC_API_KEY', - google: 'GOOGLE_API_KEY', - openrouter: 'OPENROUTER_API_KEY', -}; -const RESOURCE_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/; function defaultStoragePolicy() { return { defaultScope: 'repository', @@ -53604,7 +54001,7 @@ function createDefaultSetupConfiguration() { effort: '', }); const agents = Object.fromEntries(exports.SETUP_AGENT_TASKS.map(task => [task, defaultRole()])); - const features = Object.fromEntries(Object.keys(exports.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true])); + const features = Object.fromEntries(Object.keys(exports.SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, feature !== 'inactiveIssueClosure'])); return { features, agents, @@ -53622,6 +54019,7 @@ function createDefaultSetupConfiguration() { desiredAssigneesCount: 1, desiredReviewersCount: 1, mergeTimeout: 600, + inactivityThresholdHours: issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS, issueLocale: 'en-US', pullRequestLocale: 'en-US', commitPrefixTransforms: 'replace-slash', @@ -53668,81 +54066,92 @@ function mergeSetupConfiguration(base, overrides = {}) { manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, storage: { - secrets: mergeStoragePolicy(base.storage?.secrets, overrides.storage?.secrets), - variables: mergeStoragePolicy(base.storage?.variables, overrides.storage?.variables), + secrets: { + ...base.storage.secrets, + ...(overrides.storage?.secrets ?? {}), + overrides: { + ...base.storage.secrets.overrides, + ...(overrides.storage?.secrets?.overrides ?? {}), + }, + }, + variables: { + ...base.storage.variables, + ...(overrides.storage?.variables ?? {}), + overrides: { + ...base.storage.variables.overrides, + ...(overrides.storage?.variables?.overrides ?? {}), + }, + }, }, }; } -function validateSetupConfiguration(configuration) { - const errors = []; - const nonEmpty = [ - ['main branch', configuration.repository.mainBranch], - ['development branch', configuration.repository.developmentBranch], - ['feature branch prefix', configuration.repository.featureTree], - ['bugfix branch prefix', configuration.repository.bugfixTree], - ['hotfix branch prefix', configuration.repository.hotfixTree], - ['release branch prefix', configuration.repository.releaseTree], - ['docs branch prefix', configuration.repository.docsTree], - ['chore branch prefix', configuration.repository.choreTree], - ]; - for (const [name, value] of nonEmpty) { - if (!value.trim() || /\s/.test(value)) - errors.push(`The ${name} must be non-empty and contain no whitespace.`); - } - if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { - errors.push('Desired assignees must be between 0 and 10.'); - } - if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { - errors.push('Desired reviewers must be between 0 and 15.'); - } - if (configuration.repository.mergeTimeout < 0) - errors.push('Merge timeout cannot be negative.'); - if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { - errors.push('Bugbot comment limit must be between 1 and 100.'); - } - if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { - errors.push('Bugbot severity must be info, low, medium, or high.'); - } - if (configuration.ai.pullRequestDescriptionMode !== undefined - && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { - errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); - } - if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { - errors.push('Agent provisioning must be auto, always, or disabled.'); - } - errors.push(...validateStorageConfiguration(configuration.storage)); - for (const task of exports.SETUP_AGENT_TASKS) { - const agent = configuration.agents[task]; - if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) - errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); - if (!agent.modelProvider.trim() || !agent.model.trim()) - errors.push(`Model provider and model are required for ${task}.`); - if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) - errors.push(`Model provider and model for ${task} cannot contain whitespace.`); - } - return errors; -} -function buildSetupPlan(configuration) { - const workflowFiles = Object.entries(WORKFLOW_FILES) - .filter(([feature]) => configuration.features[feature] !== false) - .flatMap(([, files]) => files); - const issueTemplateFiles = configuration.features.issueTemplates === false - ? [] - : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') - .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); - const selectedFiles = [ - ...workflowFiles.map(file => `workflows/${file}`), - ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), - ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), + + +/***/ }), + +/***/ 87770: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildSetupPlan = buildSetupPlan; +exports.buildSetupCredentialRequirements = buildSetupCredentialRequirements; +exports.buildSetupRepositoryVariables = buildSetupRepositoryVariables; +exports.buildSetupActionInputs = buildSetupActionInputs; +const pull_request_description_1 = __nccwpck_require__(45315); +const setup_configuration_defaults_1 = __nccwpck_require__(23381); +const setup_configuration_storage_policy_1 = __nccwpck_require__(2554); +const WORKFLOW_FILES = { + issues: ['copilot_issue.yml'], + pullRequests: ['copilot_pull_request.yml'], + commits: ['copilot_commit.yml'], + issueComments: ['copilot_issue_comment.yml'], + pullRequestComments: ['copilot_pull_request_comment.yml'], + release: ['release_workflow.yml'], + hotfix: ['hotfix_workflow.yml'], + agentProvisioning: ['agent-cli-provisioning.yml'], + credentialHealth: ['copilot_credential_health.yml'], + inactiveIssueClosure: ['copilot_close_inactive_issues.yml'], +}; +const ISSUE_TEMPLATE_FILES = [ + 'config.yml', + 'feature_request.yml', + 'bug_report.yml', + 'doc_update.yml', + 'chore_task.yml', + 'help_request.yml', + 'hotfix.yml', + 'release.yml', +]; +const SECRET_BY_MODEL_PROVIDER = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + google: 'GOOGLE_API_KEY', + openrouter: 'OPENROUTER_API_KEY', +}; +function buildSetupPlan(configuration) { + const workflowFiles = Object.entries(WORKFLOW_FILES) + .filter(([feature]) => configuration.features[feature] !== false) + .flatMap(([, files]) => files); + const issueTemplateFiles = configuration.features.issueTemplates === false + ? [] + : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') + .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); + const selectedFiles = [ + ...workflowFiles.map(file => `workflows/${file}`), + ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), + ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), ]; + const credentialRequirements = buildSetupCredentialRequirements(configuration); return { configuration, workflowFiles, issueTemplateFiles, selectedFiles, variables: buildSetupRepositoryVariables(configuration), - requiredSecrets: buildRequiredSetupSecrets(configuration), - credentialRequirements: buildSetupCredentialRequirements(configuration), + requiredSecrets: credentialRequirements.map(requirement => requirement.name), + credentialRequirements, warnings: buildSetupWarnings(configuration), }; } @@ -53754,7 +54163,7 @@ function buildSetupCredentialRequirements(configuration) { requirements.set(name, { name, kind, description, provider, model }); }; add('PAT', 'workflowPat', 'A separate GitHub token owned by the bot account. It is used by workflows at runtime.'); - for (const task of exports.SETUP_AGENT_TASKS) { + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; if (agent.provider === 'cursor') { add('CURSOR_API_KEY', 'apiKey', 'Cursor API key used by the Cursor agent runtime.', 'cursor', agent.model); @@ -53785,9 +54194,9 @@ function buildSetupRepositoryVariables(configuration) { add('AGENT_MODEL', base.model); add('AGENT_EFFORT', base.effort); add('AGENT_PROVISIONING', configuration.ai.provisioningMode); - add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(exports.SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); - add('AGENT_ALLOWED_MODELS', unique(exports.SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); - for (const task of exports.SETUP_AGENT_TASKS) { + add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(setup_configuration_defaults_1.SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); + add('AGENT_ALLOWED_MODELS', unique(setup_configuration_defaults_1.SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const prefix = task.toUpperCase(); const agent = configuration.agents[task]; add(`${prefix}_PROVIDER`, agent.provider); @@ -53809,6 +54218,9 @@ function buildSetupRepositoryVariables(configuration) { add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); add('MERGE_TIMEOUT', repository.mergeTimeout); + if (configuration.features.inactiveIssueClosure !== false) { + add('INACTIVITY_THRESHOLD_HOURS', repository.inactivityThresholdHours); + } add('ISSUES_LOCALE', repository.issueLocale); add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); @@ -53845,6 +54257,7 @@ function buildSetupActionInputs(configuration) { 'desired-assignees-count': String(repository.desiredAssigneesCount), 'desired-reviewers-count': String(repository.desiredReviewersCount), 'merge-timeout': String(repository.mergeTimeout), + 'inactivity-threshold-hours': String(repository.inactivityThresholdHours), 'issues-locale': repository.issueLocale, 'pull-requests-locale': repository.pullRequestLocale, 'commit-prefix-transforms': repository.commitPrefixTransforms, @@ -53874,7 +54287,7 @@ function buildAgentActionInputs(configuration) { add('agent-model-provider', base.modelProvider); add('agent-model', base.model); add('agent-effort', base.effort); - for (const task of exports.SETUP_AGENT_TASKS) { + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; const prefix = `${task}-`; add(`${prefix}provider`, agent.provider); @@ -53884,9 +54297,6 @@ function buildAgentActionInputs(configuration) { } return result; } -function buildRequiredSetupSecrets(configuration) { - return buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); -} function buildSetupWarnings(configuration) { const warnings = []; if (configuration.features.release !== false && configuration.features.hotfix !== false) { @@ -53895,17 +54305,72 @@ function buildSetupWarnings(configuration) { if (configuration.ai.provisioningMode === 'always') { warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); } + if (configuration.features.inactiveIssueClosure !== false) { + warnings.push('Inactive issue closure is enabled; waiting issues are closed after the configured inactivity threshold and can be reopened with a new comment.'); + } if (configuration.projects.ids.trim()) { warnings.push('Project IDs must be accessible to the PAT and use the expected project column names.'); } - if (exports.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { + if (setup_configuration_defaults_1.SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); } - if (usesOrganizationStorage(configuration)) { + if ((0, setup_configuration_storage_policy_1.usesOrganizationStorage)(configuration)) { warnings.push('Organization-level Secrets and Variables require organization permissions; selected access is the safest default and repository values take precedence.'); } return warnings; } +function unique(values) { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} + + +/***/ }), + +/***/ 56637: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +/** Public setup-policy boundary. Each concern is implemented in a focused policy module. */ +__exportStar(__nccwpck_require__(23381), exports); +__exportStar(__nccwpck_require__(87770), exports); +__exportStar(__nccwpck_require__(2554), exports); +__exportStar(__nccwpck_require__(13339), exports); + + +/***/ }), + +/***/ 2554: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.resolveSetupResourceScope = resolveSetupResourceScope; +exports.getSetupResourceStoragePolicy = getSetupResourceStoragePolicy; +exports.getSetupStorageConfiguration = getSetupStorageConfiguration; +exports.resolveSetupResourceTarget = resolveSetupResourceTarget; +exports.setupResourceExists = setupResourceExists; +exports.shouldUpsertSetupResource = shouldUpsertSetupResource; +exports.validateSetupStorageAgainstRemote = validateSetupStorageAgainstRemote; +exports.usesOrganizationStorage = usesOrganizationStorage; +exports.validateStorageConfiguration = validateStorageConfiguration; +const setup_configuration_defaults_1 = __nccwpck_require__(23381); function resolveSetupResourceScope(policy, name) { return policy.overrides[name] ?? policy.defaultScope; } @@ -53913,7 +54378,7 @@ function getSetupResourceStoragePolicy(configuration, kind) { return getSetupStorageConfiguration(configuration)[kind === 'secret' ? 'secrets' : 'variables']; } function getSetupStorageConfiguration(configuration) { - const fallback = createDefaultSetupStorageConfiguration(); + const fallback = (0, setup_configuration_defaults_1.createDefaultSetupStorageConfiguration)(); return { secrets: mergeStoragePolicy(fallback.secrets, configuration.storage?.secrets), variables: mergeStoragePolicy(fallback.variables, configuration.storage?.variables), @@ -53990,17 +54455,7 @@ function usesOrganizationStorage(configuration) { const storage = getSetupStorageConfiguration(configuration); return [storage.secrets, storage.variables].some(policy => policy.defaultScope === 'organization' || Object.values(policy.overrides).includes('organization')); } -function mergeStoragePolicy(base, override) { - const fallback = base ?? defaultStoragePolicy(); - return { - ...fallback, - ...(override ?? {}), - overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, - }; -} function validateStorageConfiguration(storage) { - // Setup files created before scoped storage was introduced remain valid and - // receive the repository-level defaults through getSetupStorageConfiguration. if (!storage) return []; const errors = []; @@ -54015,7 +54470,7 @@ function validateStorageConfiguration(storage) { if (typeof policy.preserveExisting !== 'boolean') errors.push(`${kind} preserveExisting must be a boolean.`); for (const [name, scope] of Object.entries(policy.overrides ?? {})) { - if (!RESOURCE_NAME_PATTERN.test(name)) + if (!/^[A-Z][A-Z0-9_]*$/.test(name)) errors.push(`${kind} override name ${name} must be an uppercase GitHub Actions name.`); if (!['repository', 'organization'].includes(scope)) errors.push(`${kind} override ${name} must use repository or organization.`); @@ -54023,8 +54478,82 @@ function validateStorageConfiguration(storage) { } return errors; } -function unique(values) { - return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +function mergeStoragePolicy(base, override) { + const fallback = base ?? (0, setup_configuration_defaults_1.createDefaultSetupStorageConfiguration)().secrets; + return { + ...fallback, + ...(override ?? {}), + overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, + }; +} + + +/***/ }), + +/***/ 13339: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.validateSetupConfiguration = validateSetupConfiguration; +const setup_configuration_defaults_1 = __nccwpck_require__(23381); +const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); +const setup_configuration_storage_policy_1 = __nccwpck_require__(2554); +const issue_inactivity_1 = __nccwpck_require__(38572); +function validateSetupConfiguration(configuration) { + const errors = []; + const nonEmpty = [ + ['main branch', configuration.repository.mainBranch], + ['development branch', configuration.repository.developmentBranch], + ['feature branch prefix', configuration.repository.featureTree], + ['bugfix branch prefix', configuration.repository.bugfixTree], + ['hotfix branch prefix', configuration.repository.hotfixTree], + ['release branch prefix', configuration.repository.releaseTree], + ['docs branch prefix', configuration.repository.docsTree], + ['chore branch prefix', configuration.repository.choreTree], + ]; + for (const [name, value] of nonEmpty) { + if (!value.trim() || /\s/.test(value)) + errors.push(`The ${name} must be non-empty and contain no whitespace.`); + } + if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { + errors.push('Desired assignees must be between 0 and 10.'); + } + if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { + errors.push('Desired reviewers must be between 0 and 15.'); + } + if (configuration.repository.mergeTimeout < 0) + errors.push('Merge timeout cannot be negative.'); + if (!Number.isInteger(configuration.repository.inactivityThresholdHours) + || configuration.repository.inactivityThresholdHours < 1 + || configuration.repository.inactivityThresholdHours > issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS) { + errors.push(`Inactivity threshold must be between 1 and ${issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS} hours.`); + } + if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { + errors.push('Bugbot comment limit must be between 1 and 100.'); + } + if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { + errors.push('Bugbot severity must be info, low, medium, or high.'); + } + if (configuration.ai.pullRequestDescriptionMode !== undefined + && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { + errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); + } + if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { + errors.push('Agent provisioning must be auto, always, or disabled.'); + } + errors.push(...(0, setup_configuration_storage_policy_1.validateStorageConfiguration)(configuration.storage)); + for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (!agent_configuration_validation_policy_1.SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) + errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); + if (!agent.modelProvider.trim() || !agent.model.trim()) + errors.push(`Model provider and model are required for ${task}.`); + if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) + errors.push(`Model provider and model for ${task} cannot contain whitespace.`); + } + return errors; } @@ -54147,6 +54676,7 @@ exports.COPILOT_WORKFLOW_NAMES = [ 'Copilot - Commit', 'Copilot - Pull Request', 'Copilot - Pull Request Comment', + 'Copilot - Close Inactive Issues', 'Task - Hotfix', 'Task - Release', ]; @@ -54403,6 +54933,164 @@ function logProgressAssessment(progress, summary, reasoning, remaining) { } +/***/ }), + +/***/ 84579: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CloseInactiveIssuesUseCase = void 0; +const close_inactive_issues_workflow_1 = __nccwpck_require__(86288); +/** Application boundary for the scheduled inactivity-maintenance action. */ +class CloseInactiveIssuesUseCase { + constructor(issueQueryPort, issueClosurePort, clock) { + this.issueQueryPort = issueQueryPort; + this.issueClosurePort = issueClosurePort; + this.clock = clock; + this.taskId = 'CloseInactiveIssuesUseCase'; + } + async invoke(param) { + return (0, close_inactive_issues_workflow_1.runCloseInactiveIssuesWorkflow)(param, { + issueQueryPort: this.issueQueryPort, + issueClosurePort: this.issueClosurePort, + clock: this.clock, + }); + } +} +exports.CloseInactiveIssuesUseCase = CloseInactiveIssuesUseCase; + + +/***/ }), + +/***/ 86288: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.runCloseInactiveIssuesWorkflow = runCloseInactiveIssuesWorkflow; +const result_1 = __nccwpck_require__(73817); +const issue_inactivity_1 = __nccwpck_require__(38572); +const github_comment_publication_policy_1 = __nccwpck_require__(72712); +const logging_ports_1 = __nccwpck_require__(6152); +const TASK_ID = 'CloseInactiveIssuesUseCase'; +const INACTIVITY_COMMENT = (thresholdHours) => `This issue was automatically closed due to inactivity while waiting for a response. No activity was detected for at least **${thresholdHours} hours**. Reopen it and add a comment if it still needs attention.`; +/** Scans waiting issues and closes only candidates that remain inactive. */ +async function runCloseInactiveIssuesWorkflow(param, dependencies) { + const waitingLabels = unique([ + param.labels.lifecycle.awaitingMaintainer, + param.labels.lifecycle.awaitingIssueAuthor, + ]); + const activityLabel = param.labels.lifecycle.aiProcessing; + const nowMilliseconds = dependencies.clock.nowMilliseconds(); + const thresholdHours = param.inactivityThresholdHours; + try { + const candidates = await listCandidates(param, waitingLabels, dependencies.issueQueryPort); + let eligibleCount = 0; + let closedCount = 0; + let skippedCount = 0; + const errors = []; + for (const candidate of candidates) { + const initialDecision = (0, issue_inactivity_1.evaluateIssueInactivity)({ + issue: candidate, + waitingLabels, + agentActivityLabel: activityLabel, + thresholdHours, + nowMilliseconds, + }); + if (initialDecision.kind !== 'close') { + skippedCount++; + continue; + } + eligibleCount++; + try { + // Re-read both labels and updated_at immediately before the + // mutation so a comment or state transition during the scan + // invalidates the stale list snapshot. + const current = await dependencies.issueQueryPort.getOpenIssue(param.owner, param.repo, candidate.number, param.tokens.token); + if (!current || (0, issue_inactivity_1.evaluateIssueInactivity)({ + issue: current, + waitingLabels, + agentActivityLabel: activityLabel, + thresholdHours, + nowMilliseconds: dependencies.clock.nowMilliseconds(), + }).kind !== 'close') { + skippedCount++; + continue; + } + const closed = await dependencies.issueClosurePort.closeIssue(param.owner, param.repo, candidate.number, param.tokens.token); + if (!closed) { + skippedCount++; + continue; + } + closedCount++; + await dependencies.issueClosurePort.addComment(param.owner, param.repo, candidate.number, INACTIVITY_COMMENT(thresholdHours), param.tokens.token); + (0, logging_ports_1.logInfo)(`Issue #${candidate.number} closed after inactivity.`); + } + catch (error) { + const message = `Unable to close issue #${candidate.number} after inactivity.`; + (0, logging_ports_1.logError)(message); + errors.push(`${message} ${safeErrorMessage(error)}`); + } + } + (0, logging_ports_1.logDebugInfo)(`${TASK_ID}: scanned=${candidates.length}, eligible=${eligibleCount}, closed=${closedCount}, skipped=${skippedCount}.`); + return [new result_1.Result({ + id: TASK_ID, + success: errors.length === 0, + executed: closedCount > 0 || eligibleCount > 0, + steps: buildSteps(candidates.length, closedCount, skippedCount), + payload: { + scanned: candidates.length, + eligible: eligibleCount, + closed: closedCount, + skipped: skippedCount, + }, + errors, + })]; + } + catch (error) { + const message = 'Unable to scan issues for inactivity closure.'; + (0, logging_ports_1.logError)(message); + return [new result_1.Result({ + id: TASK_ID, + success: false, + executed: true, + steps: [message], + errors: [`${message} ${safeErrorMessage(error)}`], + })]; + } +} +async function listCandidates(param, waitingLabels, queryPort) { + const candidates = []; + for (const label of waitingLabels) { + candidates.push(...await queryPort.listOpenIssuesByLabel(param.owner, param.repo, label, param.tokens.token)); + } + const uniqueCandidates = new Map(); + for (const candidate of candidates) + uniqueCandidates.set(candidate.number, candidate); + return [...uniqueCandidates.values()]; +} +function buildSteps(scanned, closed, skipped) { + const steps = [`Scanned ${scanned} open issue(s) waiting for a response.`]; + if (closed > 0) + steps.push(`Closed ${closed} issue(s) after the inactivity threshold.`); + if (skipped > 0) + steps.push(`Skipped ${skipped} candidate(s) because they were no longer eligible.`); + if (closed === 0) + steps.push('No issue was closed for inactivity.'); + return steps; +} +function unique(values) { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} +function safeErrorMessage(error) { + const message = (0, github_comment_publication_policy_1.sanitizePublishedError)(error instanceof Error ? error.message : error); + return message || 'Unknown provider error.'; +} + + /***/ }), /***/ 76549: @@ -54414,19 +55102,19 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.validateReleaseInput = validateReleaseInput; exports.normalizeVersion = normalizeVersion; exports.versionForRelease = versionForRelease; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const application_error_1 = __nccwpck_require__(75999); const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/; function validateReleaseInput(input) { if (!input.version.length) - return `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`; + return `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`; if (!input.title.length) - return `${constants_1.INPUT_KEYS.SINGLE_ACTION_TITLE} is not set.`; + return `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_TITLE} is not set.`; if (!input.changelog.length) - return `${constants_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`; + return `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`; const normalized = normalizeVersion(input.version); return normalized === undefined - ? `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0). Got: ${input.version}` + ? `${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0). Got: ${input.version}` : undefined; } function normalizeVersion(version) { @@ -54554,7 +55242,7 @@ exports.CreateTagUseCase = CreateTagUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runCreateTag = runCreateTag; const result_1 = __nccwpck_require__(73817); -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); async function runCreateTag(param, taskId, repositoryTagPort) { const validationFailure = validateTagInput(param, taskId); @@ -54574,7 +55262,7 @@ async function runCreateTag(param, taskId, repositoryTagPort) { function validateTagInput(param, taskId) { if (param.singleAction.version.length === 0) { (0, logging_ports_1.logError)('Version is not set.'); - return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); } if (param.currentConfiguration.releaseBranch === undefined) { (0, logging_ports_1.logError)('Working branch not found in configuration.'); @@ -54762,6 +55450,37 @@ async function findIssueBranch(param, repository) { } +/***/ }), + +/***/ 57389: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createInitialSetupRequest = createInitialSetupRequest; +/** Converts the legacy execution aggregate into the setup use case's explicit request. */ +function createInitialSetupRequest(execution) { + return { + owner: execution.owner, + repo: execution.repo, + token: execution.tokens.token, + labels: execution.labels, + issueTypes: execution.issueTypes, + setupConfiguration: asObject(execution.inputs?.setupConfiguration), + setupCredentials: asObject(execution.inputs?.setupCredentials), + setupRemoteConfiguration: asObject(execution.inputs?.setupRemoteConfiguration), + workflowUpdates: asStringArray(execution.inputs?.setupWorkflowUpdates), + }; +} +function asObject(value) { + return value && typeof value === 'object' ? value : undefined; +} +function asStringArray(value) { + return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []; +} + + /***/ }), /***/ 84837: @@ -54772,6 +55491,7 @@ async function findIssueBranch(param, repository) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.InitialSetupUseCase = void 0; const initial_setup_workflow_1 = __nccwpck_require__(18079); +const initial_setup_request_1 = __nccwpck_require__(57389); /** Application boundary for provisioning a repository for Copilot automation. */ class InitialSetupUseCase { constructor(authenticatedUserPort, initialLabelProvisioningPort, issueTypeProvisioningPort, latestTagQueryPort, repositoryDefaultBranchPort, repositoryTagPort, setupWorkspacePort, setupRepositoryVariablesPort, setupRepositorySecretsPort, setupRemoteConfigurationReadPort) { @@ -54788,7 +55508,7 @@ class InitialSetupUseCase { this.taskId = 'InitialSetupUseCase'; } async invoke(param) { - return await (0, initial_setup_workflow_1.runInitialSetupWorkflow)(param, { + return await (0, initial_setup_workflow_1.runInitialSetupWorkflow)((0, initial_setup_request_1.createInitialSetupRequest)(param), { authenticatedUserPort: this.authenticatedUserPort, initialLabelProvisioningPort: this.initialLabelProvisioningPort, issueTypeProvisioningPort: this.issueTypeProvisioningPort, @@ -54818,46 +55538,45 @@ const result_1 = __nccwpck_require__(73817); const version_policy_1 = __nccwpck_require__(8381); const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); -const setup_configuration_policy_1 = __nccwpck_require__(56637); +const setup_resource_provisioning_1 = __nccwpck_require__(94894); const TASK_ID = 'InitialSetupUseCase'; /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ -async function runInitialSetupWorkflow(param, dependencies) { +async function runInitialSetupWorkflow(request, dependencies) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(TASK_ID)} Executing ${TASK_ID}.`); const steps = []; const errors = []; try { - const setupConfiguration = getSetupConfiguration(param); - if (!dependencies.setupWorkspacePort.hasValidToken(param.tokens.token)) { + const setupConfiguration = request.setupConfiguration; + if (!dependencies.setupWorkspacePort.hasValidToken(request.token)) { (0, logging_ports_1.logInfo)(' 🛑 Setup requires the setup PAT provided for this command with a valid token.'); errors.push('A valid setup PAT must be provided to run setup. It is separate from the workflow PAT Secret.'); return [buildResult(errors, steps)]; } (0, logging_ports_1.logInfo)('📋 Ensuring .github and copying setup files...'); - const workflowUpdates = getWorkflowUpdates(param); const workspaceSelection = { features: setupConfiguration?.features, - ...(workflowUpdates.length > 0 ? { + ...(request.workflowUpdates.length > 0 ? { updateExistingWorkflows: true, - approvedWorkflowFiles: workflowUpdates, + approvedWorkflowFiles: request.workflowUpdates, } : {}), }; const filesResult = dependencies.setupWorkspacePort.prepare(workspaceSelection); steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); (0, logging_ports_1.logInfo)('🔐 Checking GitHub access...'); - const githubAccess = await verifyGitHubAccess(param, dependencies.authenticatedUserPort); + const githubAccess = await verifyGitHubAccess(request, dependencies.authenticatedUserPort); if (!githubAccess.success) { errors.push(...githubAccess.errors); return [buildResult(errors, steps)]; } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); - const remoteConfiguration = await resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors); - const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration); + const remoteConfiguration = await (0, setup_resource_provisioning_1.resolveRemoteConfiguration)(request, dependencies, setupConfiguration, errors); + const secrets = await (0, setup_resource_provisioning_1.ensureRepositorySecrets)(request, dependencies, setupConfiguration, remoteConfiguration); if (secrets.step) steps.push(secrets.step); if (secrets.errors.length > 0) errors.push(...secrets.errors); (0, logging_ports_1.logInfo)('🏷️ Checking configured and progress labels...'); - const labels = await ensureInitialLabels(param, dependencies.initialLabelProvisioningPort); + const labels = await ensureInitialLabels(request, dependencies.initialLabelProvisioningPort); if (!labels.completed) { errors.push(labels.error); } @@ -54866,19 +55585,19 @@ async function runInitialSetupWorkflow(param, dependencies) { appendLabelSummary(steps, errors, labels.progress, 'Progress labels'); } (0, logging_ports_1.logInfo)('📋 Checking issue types...'); - const issueTypes = await ensureIssueTypes(param, dependencies.issueTypeProvisioningPort); + const issueTypes = await ensureIssueTypes(request, dependencies.issueTypeProvisioningPort); if (!issueTypes.success) { errors.push(...issueTypes.errors); } else { steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration); + const variables = await (0, setup_resource_provisioning_1.ensureRepositoryVariables)(request, dependencies, setupConfiguration, remoteConfiguration); if (variables.step) steps.push(variables.step); if (variables.errors.length > 0) errors.push(...variables.errors); - const defaultVersion = await ensureDefaultVersion(param, dependencies, setupConfiguration); + const defaultVersion = await ensureDefaultVersion(request, dependencies, setupConfiguration); if (defaultVersion.step) steps.push(defaultVersion.step); if (defaultVersion.error) @@ -54891,9 +55610,9 @@ async function runInitialSetupWorkflow(param, dependencies) { return [buildResult(errors, steps)]; } } -async function verifyGitHubAccess(param, repository) { +async function verifyGitHubAccess(request, repository) { try { - const user = await repository.getUserFromToken(param.tokens.token); + const user = await repository.getUserFromToken(request.token); return { success: true, user, errors: [] }; } catch (error) { @@ -54901,9 +55620,9 @@ async function verifyGitHubAccess(param, repository) { return { success: false, errors: [`Could not verify GitHub access: ${error}`] }; } } -async function ensureInitialLabels(param, repository) { +async function ensureInitialLabels(request, repository) { try { - const summary = await repository.ensureInitialLabels(param.owner, param.repo, param.labels, param.tokens.token); + const summary = await repository.ensureInitialLabels(request.owner, request.repo, request.labels, request.token); return { completed: true, ...summary }; } catch (error) { @@ -54912,9 +55631,9 @@ async function ensureInitialLabels(param, repository) { return { completed: false, error: message }; } } -async function ensureIssueTypes(param, repository) { +async function ensureIssueTypes(request, repository) { try { - const result = await repository.ensureIssueTypes(param.owner, param.issueTypes, param.tokens.token); + const result = await repository.ensureIssueTypes(request.owner, request.issueTypes, request.token); return { success: result.errors.length === 0, created: result.created, @@ -54927,7 +55646,7 @@ async function ensureIssueTypes(param, repository) { return { success: false, created: 0, existing: 0, errors: [`Error ensuring issue types: ${error}`] }; } } -async function ensureDefaultVersion(param, dependencies, setupConfiguration) { +async function ensureDefaultVersion(request, dependencies, setupConfiguration) { if (setupConfiguration?.createInitialTag === false) { return { step: '⏭️ Initial version tag creation disabled by setup configuration.' }; } @@ -54938,16 +55657,16 @@ async function ensureDefaultVersion(param, dependencies, setupConfiguration) { return {}; } (0, logging_ports_1.logInfo)(`🏷️ No version tags found. Creating default tag ${version_policy_1.DEFAULT_INITIAL_TAG}...`); - const defaultBranch = await dependencies.repositoryDefaultBranchPort.getDefaultBranch(param.owner, param.repo, param.tokens.token); + const defaultBranch = await dependencies.repositoryDefaultBranchPort.getDefaultBranch(request.owner, request.repo, request.token); if (!defaultBranch) { const message = 'Could not get default branch to create initial version tag.'; (0, logging_ports_1.logError)(message); return { error: message }; } - const sha = await dependencies.repositoryTagPort.createTag(param.owner, param.repo, defaultBranch, version_policy_1.DEFAULT_INITIAL_TAG, param.tokens.token); + const sha = await dependencies.repositoryTagPort.createTag(request.owner, request.repo, defaultBranch, version_policy_1.DEFAULT_INITIAL_TAG, request.token); return sha ? { step: `✅ Default version tag ${version_policy_1.DEFAULT_INITIAL_TAG} created on branch ${defaultBranch}. Run \`git fetch --tags\` to update local refs.` } - : { error: `Failed to create tag ${version_policy_1.DEFAULT_INITIAL_TAG} on ${param.owner}/${param.repo}` }; + : { error: `Failed to create tag ${version_policy_1.DEFAULT_INITIAL_TAG} on ${request.owner}/${request.repo}` }; } catch (error) { const message = `Error ensuring default version: ${error}`; @@ -54955,144 +55674,6 @@ async function ensureDefaultVersion(param, dependencies, setupConfiguration) { return { error: message }; } } -function getSetupConfiguration(param) { - const configuration = param.inputs?.setupConfiguration; - return configuration && typeof configuration === 'object' - ? configuration - : undefined; -} -function getWorkflowUpdates(param) { - const updates = param.inputs?.setupWorkflowUpdates; - return Array.isArray(updates) ? updates.filter((file) => typeof file === 'string') : []; -} -async function ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration) { - if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { - return { errors: [] }; - } - try { - const desired = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration); - const groups = groupResources(desired, 'variable', setupConfiguration, remoteConfiguration); - const result = await upsertVariableGroups(param, dependencies.setupRepositoryVariablesPort, groups); - if (result.errors.length > 0) - return { errors: result.errors }; - return { - step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, - errors: [], - }; - } - catch (error) { - const message = `Error configuring repository Variables: ${error}`; - (0, logging_ports_1.logError)(message); - return { errors: [message] }; - } -} -async function ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration) { - if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { - return { errors: [] }; - } - const credentials = getSetupCredentialCollection(param); - if (!credentials) { - return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; - } - const values = [ - ...(credentials.workflowPat ? [credentials.workflowPat] : []), - ...credentials.apiKeys, - ]; - if (values.length === 0) - return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; - try { - const groups = groupResources(values, 'secret', setupConfiguration, remoteConfiguration); - const result = await upsertSecretGroups(param, dependencies.setupRepositorySecretsPort, groups); - if (result.errors.length > 0) - return { errors: result.errors }; - return { - step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, - errors: [], - }; - } - catch (error) { - const message = `Error configuring repository Secrets: ${error}`; - (0, logging_ports_1.logError)(message); - return { errors: [message] }; - } -} -async function resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors) { - const provided = param.inputs?.setupRemoteConfiguration; - if (provided && typeof provided === 'object') - return provided; - if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) - return undefined; - try { - return await dependencies.setupRemoteConfigurationReadPort.inspect(param.owner, param.repo, param.tokens.token); - } - catch (error) { - const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; - (0, logging_ports_1.logError)(message); - if ((0, setup_configuration_policy_1.usesOrganizationStorage)(setupConfiguration)) - errors.push(message); - return undefined; - } -} -function groupResources(resources, kind, configuration, remoteConfiguration) { - const groups = new Map(); - for (const resource of resources) { - // Secret values reach this workflow only after the user chose keep/replace. - // Variables, however, are always generated from the selected setup contract, - // so preserveExisting must be applied here to avoid shadowing inherited values. - if (kind === 'variable' && !(0, setup_configuration_policy_1.shouldUpsertSetupResource)(configuration, kind, resource.name, remoteConfiguration)) - continue; - const target = (0, setup_configuration_policy_1.resolveSetupResourceTarget)(configuration, kind, resource.name, remoteConfiguration); - const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; - const group = groups.get(key) ?? { target, resources: [] }; - group.resources.push(resource); - groups.set(key, group); - } - return [...groups.values()]; -} -async function upsertVariableGroups(param, port, groups) { - let created = 0; - let updated = 0; - const errors = []; - for (const group of groups) { - if (group.target.scope === 'organization' && !port.upsertScopedVariables) { - errors.push('Organization Variable provisioning is not available in this installation.'); - continue; - } - const result = group.target.scope === 'organization' - ? await port.upsertScopedVariables(param.owner, param.repo, param.tokens.token, group.target, group.resources) - : await port.upsert(param.owner, param.repo, param.tokens.token, group.resources); - created += result.created; - updated += result.updated; - errors.push(...result.errors); - } - return { created, updated, errors }; -} -async function upsertSecretGroups(param, port, groups) { - let created = 0; - let updated = 0; - let skipped = 0; - const errors = []; - for (const group of groups) { - if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { - errors.push('Organization Secret provisioning is not available in this installation.'); - continue; - } - const result = group.target.scope === 'organization' - ? await port.upsertScopedSecrets(param.owner, param.repo, param.tokens.token, group.target, group.resources) - : await port.upsertSecrets(param.owner, param.repo, param.tokens.token, group.resources); - created += result.created; - updated += result.updated; - skipped += result.skipped; - errors.push(...result.errors); - } - return { created, updated, skipped, errors }; -} -function getSetupCredentialCollection(param) { - const credentials = param.inputs?.setupCredentials; - if (!credentials || typeof credentials !== 'object') - return undefined; - return credentials; -} function appendLabelSummary(steps, errors, summary, labelType) { if (summary.errors.length > 0) { errors.push(...summary.errors); @@ -55335,7 +55916,7 @@ exports.PublishGithubActionUseCase = PublishGithubActionUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runPublishGithubAction = runPublishGithubAction; const result_1 = __nccwpck_require__(73817); -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); async function runPublishGithubAction(param, taskId, repositoryTagPort, repositoryReleasePort) { const validationFailure = validateVersion(param, taskId); @@ -55363,7 +55944,7 @@ function validateVersion(param, taskId) { if (param.singleAction.version.length > 0) return undefined; (0, logging_ports_1.logError)('Version is not set.'); - return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); } function successResult(taskId, sourceTag, targetTag, releaseId) { (0, logging_ports_1.logInfo)(`Updated release \`${targetTag}\` from \`${sourceTag}\`: ${releaseId}`); @@ -55541,6 +56122,144 @@ function failure(taskId, message) { } +/***/ }), + +/***/ 94894: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ensureRepositoryVariables = ensureRepositoryVariables; +exports.ensureRepositorySecrets = ensureRepositorySecrets; +exports.resolveRemoteConfiguration = resolveRemoteConfiguration; +exports.groupSetupResources = groupSetupResources; +const setup_configuration_policy_1 = __nccwpck_require__(56637); +const logging_ports_1 = __nccwpck_require__(6152); +async function ensureRepositoryVariables(context, dependencies, setupConfiguration, remoteConfiguration) { + if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { + return { errors: [] }; + } + try { + const desired = (0, setup_configuration_policy_1.buildSetupRepositoryVariables)(setupConfiguration); + const groups = groupSetupResources(desired, 'variable', setupConfiguration, remoteConfiguration); + const result = await upsertVariableGroups(context, dependencies.setupRepositoryVariablesPort, groups); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Variables: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +async function ensureRepositorySecrets(context, dependencies, setupConfiguration, remoteConfiguration) { + if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { + return { errors: [] }; + } + const credentials = context.setupCredentials; + if (!credentials) { + return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; + } + const values = [ + ...(credentials.workflowPat ? [credentials.workflowPat] : []), + ...credentials.apiKeys, + ]; + if (values.length === 0) + return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; + try { + const groups = groupSetupResources(values, 'secret', setupConfiguration, remoteConfiguration); + const result = await upsertSecretGroups(context, dependencies.setupRepositorySecretsPort, groups); + if (result.errors.length > 0) + return { errors: result.errors }; + return { + step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, + errors: [], + }; + } + catch (error) { + const message = `Error configuring repository Secrets: ${error}`; + (0, logging_ports_1.logError)(message); + return { errors: [message] }; + } +} +async function resolveRemoteConfiguration(context, dependencies, setupConfiguration, errors) { + if (context.setupRemoteConfiguration) + return context.setupRemoteConfiguration; + if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) + return undefined; + try { + return await dependencies.setupRemoteConfigurationReadPort.inspect(context.owner, context.repo, context.token); + } + catch (error) { + const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + (0, logging_ports_1.logError)(message); + if ((0, setup_configuration_policy_1.usesOrganizationStorage)(setupConfiguration)) + errors.push(message); + return undefined; + } +} +/** Groups resources by their resolved storage target so each provider call is scoped explicitly. */ +function groupSetupResources(resources, kind, configuration, remoteConfiguration) { + const groups = new Map(); + for (const resource of resources) { + // Secret values reach this workflow only after the user chose keep/replace. + // Variables are generated from the selected setup contract, so preserving + // an inherited value must happen before the provider call is assembled. + if (kind === 'variable' && !(0, setup_configuration_policy_1.shouldUpsertSetupResource)(configuration, kind, resource.name, remoteConfiguration)) + continue; + const target = (0, setup_configuration_policy_1.resolveSetupResourceTarget)(configuration, kind, resource.name, remoteConfiguration); + const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; + const group = groups.get(key) ?? { target, resources: [] }; + group.resources.push(resource); + groups.set(key, group); + } + return [...groups.values()]; +} +async function upsertVariableGroups(context, port, groups) { + let created = 0; + let updated = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedVariables) { + errors.push('Organization Variable provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedVariables(context.owner, context.repo, context.token, group.target, group.resources) + : await port.upsert(context.owner, context.repo, context.token, group.resources); + created += result.created; + updated += result.updated; + errors.push(...result.errors); + } + return { created, updated, errors }; +} +async function upsertSecretGroups(context, port, groups) { + let created = 0; + let updated = 0; + let skipped = 0; + const errors = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { + errors.push('Organization Secret provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedSecrets(context.owner, context.repo, context.token, group.target, group.resources) + : await port.upsertSecrets(context.owner, context.repo, context.token, group.resources); + created += result.created; + updated += result.updated; + skipped += result.skipped; + errors.push(...result.errors); + } + return { created, updated, skipped, errors }; +} + + /***/ }), /***/ 18277: @@ -56257,7 +56976,7 @@ exports.ExecutionBranchVersionResolver = ExecutionBranchVersionResolver; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveEventIssueNumber = resolveEventIssueNumber; exports.resolveSingleActionIssueNumber = resolveSingleActionIssueNumber; -const constants_1 = __nccwpck_require__(15415); +const input_keys_1 = __nccwpck_require__(88539); const positive_integer_policy_1 = __nccwpck_require__(19879); const title_utils_1 = __nccwpck_require__(46267); function resolveEventIssueNumber(execution) { @@ -56275,7 +56994,7 @@ function resolveEventIssueNumber(execution) { return positiveIssueNumberOrUndefined(execution.issueNumber); } async function resolveSingleActionIssueNumber(execution, issueRepository) { - const configuredIssue = execution.inputs?.[constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]; + const configuredIssue = execution.inputs?.[input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]; if (configuredIssue !== undefined && configuredIssue !== null && String(configuredIssue).trim() !== '') { const issueNumber = (0, positive_integer_policy_1.parsePositiveSafeInteger)(configuredIssue); return issueNumber === undefined ? undefined : setIssueNumber(execution, issueNumber); @@ -56851,7 +57570,7 @@ const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); const single_action_workflow_1 = __nccwpck_require__(6130); class SingleActionUseCase { - constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase) { + constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase, closeInactiveIssuesUseCase) { this.deployedActionUseCase = deployedActionUseCase; this.publishGithubActionUseCase = publishGithubActionUseCase; this.createReleaseUseCase = createReleaseUseCase; @@ -56861,6 +57580,7 @@ class SingleActionUseCase { this.checkProgressUseCase = checkProgressUseCase; this.detectPotentialProblemsUseCase = detectPotentialProblemsUseCase; this.recommendStepsUseCase = recommendStepsUseCase; + this.closeInactiveIssuesUseCase = closeInactiveIssuesUseCase; this.taskId = "SingleActionUseCase"; } async invoke(param) { @@ -56879,6 +57599,7 @@ class SingleActionUseCase { checkProgressUseCase: this.checkProgressUseCase, detectPotentialProblemsUseCase: this.detectPotentialProblemsUseCase, recommendStepsUseCase: this.recommendStepsUseCase, + closeInactiveIssuesUseCase: this.closeInactiveIssuesUseCase, }); } } @@ -56912,8 +57633,9 @@ async function runSingleActionWorkflow(param, taskId, ports) { { active: param.singleAction.isCheckProgressAction, useCase: ports.checkProgressUseCase }, { active: param.singleAction.isDetectPotentialProblemsAction, useCase: ports.detectPotentialProblemsUseCase }, { active: param.singleAction.isRecommendStepsAction, useCase: ports.recommendStepsUseCase }, - ].find(({ active }) => active); - if (!action) + { active: param.singleAction.isCloseInactiveIssuesAction, useCase: ports.closeInactiveIssuesUseCase }, + ].find(({ active, useCase }) => active && useCase !== undefined); + if (!action || !action.useCase) return []; try { return await action.useCase.invoke(param); @@ -56946,10 +57668,10 @@ exports.applyDetectedFindings = applyDetectedFindings; const prepare_bugbot_findings_1 = __nccwpck_require__(85016); const mark_findings_resolved_use_case_1 = __nccwpck_require__(96963); const publish_findings_use_case_1 = __nccwpck_require__(88442); -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); const pull_request_review_errors_1 = __nccwpck_require__(46445); function prepareDetectedFindings(execution, response) { - return (0, prepare_bugbot_findings_1.prepareBugbotFindings)(response, execution.ai?.getAiIgnoreFiles?.() ?? [], execution.ai?.getBugbotMinSeverity?.(), execution.ai?.getBugbotCommentLimit?.() ?? constants_1.BUGBOT_MAX_COMMENTS); + return (0, prepare_bugbot_findings_1.prepareBugbotFindings)(response, execution.ai?.getAiIgnoreFiles?.() ?? [], execution.ai?.getBugbotMinSeverity?.(), execution.ai?.getBugbotCommentLimit?.() ?? bugbot_constants_1.BUGBOT_MAX_COMMENTS); } async function applyDetectedFindings(execution, context, prepared, publicationPorts, resolutionPorts) { try { @@ -58285,12 +59007,12 @@ async function restoreStashedChanges(gitCommitPort) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.applyCommentLimit = applyCommentLimit; -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); /** * Applies the max-comments limit: returns the first N findings to publish individually, * and overflow count + titles for a single "revisar en local" summary comment. */ -function applyCommentLimit(findings, maxComments = constants_1.BUGBOT_MAX_COMMENTS) { +function applyCommentLimit(findings, maxComments = bugbot_constants_1.BUGBOT_MAX_COMMENTS) { if (findings.length <= maxComments) { return { toPublish: findings, overflowCount: 0, overflowTitles: [] }; } @@ -58505,7 +59227,7 @@ exports.markerRegexForFinding = markerRegexForFinding; exports.replaceMarkerInBody = replaceMarkerInBody; exports.extractTitleFromBody = extractTitleFromBody; exports.buildCommentBody = buildCommentBody; -const constants_1 = __nccwpck_require__(15415); +const bugbot_constants_1 = __nccwpck_require__(51389); const application_error_1 = __nccwpck_require__(75999); const github_comment_publication_policy_1 = __nccwpck_require__(72712); /** Maximum lossless finding identity accepted by the marker contract. */ @@ -58544,13 +59266,13 @@ function buildMarker(findingId, resolved, fingerprint, resolution) { const safeResolution = resolved && resolution && ['fixed', 'obsolete', 'dismissed'].includes(resolution) ? ` finding_resolution:"${resolution}"` : ''; - return ``; + return ``; } function parseMarker(body) { if (!body) return []; const results = []; - const regex = new RegExp(``, "g"); + const regex = new RegExp(``, "g"); let m; while ((m = regex.exec(body)) !== null) { results.push({ @@ -58571,7 +59293,7 @@ function markerRegexForFinding(findingId) { const idForRegex = SAFE_FINDING_ID_REGEX_CHARS.test(safeId) ? safeId : safeId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return new RegExp(``, "g"); + return new RegExp(``, "g"); } /** * Find the marker for this finding in body (using same pattern as parseMarker) and replace it. @@ -63051,6 +63773,7 @@ exports.ACTIONS = { CHECK_PROGRESS: 'check_progress_action', DETECT_POTENTIAL_PROBLEMS: 'detect_potential_problems_action', RECOMMEND_STEPS: 'recommend_steps_action', + CLOSE_INACTIVE_ISSUES: 'close_inactive_issues_action', }; @@ -63335,6 +64058,7 @@ const label_branch_policy_1 = __nccwpck_require__(53318); const commit_1 = __nccwpck_require__(57525); const config_1 = __nccwpck_require__(90450); const github_user_policy_1 = __nccwpck_require__(84403); +const issue_inactivity_1 = __nccwpck_require__(38572); class Execution { get eventName() { return this.inputs?.eventName ?? ''; @@ -63426,6 +64150,7 @@ class Execution { this.project = components.projects; this.workflows = components.workflows; this.tokenUser = components.tokenUser; + this.inactivityThresholdHours = components.inactivityThresholdHours ?? issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS; this.currentConfiguration = new config_1.Config({}); this.inputs = components.inputs; this.welcome = components.welcome; @@ -64310,6 +65035,9 @@ class SingleAction { get isRecommendStepsAction() { return this.currentSingleAction === action_types_1.ACTIONS.RECOMMEND_STEPS; } + get isCloseInactiveIssuesAction() { + return this.currentSingleAction === action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES; + } get enabledSingleAction() { return this.currentSingleAction.length > 0; } @@ -64335,6 +65063,7 @@ class SingleAction { action_types_1.ACTIONS.CHECK_PROGRESS, action_types_1.ACTIONS.DETECT_POTENTIAL_PROBLEMS, action_types_1.ACTIONS.RECOMMEND_STEPS, + action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, ]; /** * Actions that throw an error if the last step failed @@ -64344,6 +65073,7 @@ class SingleAction { action_types_1.ACTIONS.CREATE_RELEASE, action_types_1.ACTIONS.DEPLOYED, action_types_1.ACTIONS.CREATE_TAG, + action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, ]; /** * Actions that do not require an issue @@ -64351,6 +65081,7 @@ class SingleAction { this.actionsWithoutIssue = [ action_types_1.ACTIONS.THINK, action_types_1.ACTIONS.INITIAL_SETUP, + action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, ]; this.isIssue = false; this.isPullRequest = false; @@ -65441,7 +66172,7 @@ function extractReasoningFromParts(parts) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.AgentCapabilityAdapter = void 0; -const constants_1 = __nccwpck_require__(15415); +const agent_constants_1 = __nccwpck_require__(46927); const logger_1 = __nccwpck_require__(91151); const provider_cli_adapter_1 = __nccwpck_require__(18199); const agent_configuration_policy_1 = __nccwpck_require__(49616); @@ -65455,7 +66186,7 @@ class AgentCapabilityAdapter { const output = await this.cliAdapter.execute({ configuration: taskConfiguration, prompt: this.addEffortInstruction(request.prompt, taskConfiguration.effort), - timeoutMs: constants_1.AGENT_REQUEST_TIMEOUT_MS, + timeoutMs: agent_constants_1.AGENT_REQUEST_TIMEOUT_MS, }); return request.mapCliOutput(output); } @@ -65474,6 +66205,19 @@ class AgentCapabilityAdapter { exports.AgentCapabilityAdapter = AgentCapabilityAdapter; +/***/ }), + +/***/ 46927: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.AGENT_REQUEST_TIMEOUT_MS = void 0; +/** Maximum time allowed for one external agent CLI request. */ +exports.AGENT_REQUEST_TIMEOUT_MS = 900000; + + /***/ }), /***/ 27725: @@ -66497,35 +67241,96 @@ exports.IssueContentRepository = IssueContentRepository; /***/ }), -/***/ 59699: +/***/ 28868: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.IssueLabelProvisioningRepository = void 0; -const initial_label_provisioning_policy_1 = __nccwpck_require__(73160); -const logger_1 = __nccwpck_require__(91151); -const github_error_policy_1 = __nccwpck_require__(58791); +exports.IssueInactivityRepository = void 0; const github_pagination_policy_1 = __nccwpck_require__(44812); -class IssueLabelProvisioningRepository { +/** Reads the provider's issue activity timestamp and waiting-state labels. */ +class IssueInactivityRepository { constructor(githubClient) { this.githubClient = githubClient; - this.ensureInitialLabels = async (owner, repository, labels, token) => { + this.listOpenIssuesByLabel = async (owner, repository, label, token) => { const client = this.githubClient.getClient(token); - const inventory = await this.listLabelsForRepo(client, owner, repository); - const plan = (0, initial_label_provisioning_policy_1.buildInitialLabelProvisioningPlan)(labels, inventory.map(label => label.name)); - const context = { client, owner, repository }; - return { - configured: await this.provisionMissingLabels(context, plan.configured), - progress: await this.provisionMissingLabels(context, plan.progress), - }; + const issues = []; + for await (const response of client.paginate.iterator(client.rest.issues.listForRepo, { + owner, + repo: repository, + state: 'open', + labels: label, + sort: 'updated', + direction: 'asc', + per_page: 100, + })) { + const page = (0, github_pagination_policy_1.requireArrayPage)(response.data, 'open issues'); + issues.push(...page.map(toSnapshot)); + } + return issues; }; - this.listLabelsForRepo = async (client, owner, repository) => { - const labels = []; - for await (const page of client.paginate.iterator(client.rest.issues.listLabelsForRepo, { owner, repo: repository, per_page: 100 })) { - const labelsPage = (0, github_pagination_policy_1.requireArrayPage)(page.data, 'repository labels'); - labels.push(...labelsPage.map(label => ({ + this.getOpenIssue = async (owner, repository, issueNumber, token) => { + const client = this.githubClient.getClient(token); + const response = await client.rest.issues.get({ + owner, + repo: repository, + issue_number: issueNumber, + }); + if (response.data.state !== 'open') + return undefined; + return toSnapshot(response.data); + }; + } +} +exports.IssueInactivityRepository = IssueInactivityRepository; +function toSnapshot(issue) { + if (!Number.isSafeInteger(issue.number) || issue.number < 1) { + throw new Error('GitHub issue response contained an invalid issue number.'); + } + return { + number: issue.number, + updatedAt: issue.updated_at ?? undefined, + isPullRequest: issue.pull_request !== undefined, + labels: (issue.labels ?? []).flatMap(label => { + const name = typeof label === 'string' ? label : label.name; + return name?.trim() ? [name] : []; + }), + }; +} + + +/***/ }), + +/***/ 59699: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.IssueLabelProvisioningRepository = void 0; +const initial_label_provisioning_policy_1 = __nccwpck_require__(73160); +const logger_1 = __nccwpck_require__(91151); +const github_error_policy_1 = __nccwpck_require__(58791); +const github_pagination_policy_1 = __nccwpck_require__(44812); +class IssueLabelProvisioningRepository { + constructor(githubClient) { + this.githubClient = githubClient; + this.ensureInitialLabels = async (owner, repository, labels, token) => { + const client = this.githubClient.getClient(token); + const inventory = await this.listLabelsForRepo(client, owner, repository); + const plan = (0, initial_label_provisioning_policy_1.buildInitialLabelProvisioningPlan)(labels, inventory.map(label => label.name)); + const context = { client, owner, repository }; + return { + configured: await this.provisionMissingLabels(context, plan.configured), + progress: await this.provisionMissingLabels(context, plan.progress), + }; + }; + this.listLabelsForRepo = async (client, owner, repository) => { + const labels = []; + for await (const page of client.paginate.iterator(client.rest.issues.listLabelsForRepo, { owner, repo: repository, per_page: 100 })) { + const labelsPage = (0, github_pagination_policy_1.requireArrayPage)(page.data, 'repository labels'); + labels.push(...labelsPage.map(label => ({ name: label.name, color: label.color, description: label.description ?? null, @@ -69594,7 +70399,7 @@ function encryptSecret(value, base64PublicKey) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ActivePreviousWorkflowRunsRepository = void 0; -const constants_1 = __nccwpck_require__(15415); +const workflow_status_1 = __nccwpck_require__(1462); const workflow_runs_retry_1 = __nccwpck_require__(86434); const NO_OP_DELAY_PORT = { wait: async () => undefined }; const SYSTEM_CLOCK = { nowMilliseconds: () => Date.now() }; @@ -69634,7 +70439,7 @@ class ActivePreviousWorkflowRunsRepository { return (0, workflow_runs_retry_1.withWorkflowRunsRetry)(async () => { let activeRunCount = 0; // Keep one complete sequential traversal: GitHub cannot safely express - // the seven shared workflow names, five active statuses, or the strict + // the eight shared workflow names, five active statuses, or the strict // lower-ID predicate in this endpoint. Do not add provider filters or // early-stop on page order; a matching run may occur on a later page. // The residual cost is deep-history pagination, with retries restarting @@ -69668,7 +70473,7 @@ function isActivePreviousRun(run, query, workflowNames) { return typeof run.name === 'string' && workflowNames.includes(run.name) && run.id < query.currentRunId - && constants_1.WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); + && workflow_status_1.WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); } @@ -69876,6 +70681,36 @@ function firstNumericValue(...values) { } +/***/ }), + +/***/ 1462: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.WORKFLOW_ACTIVE_STATUSES = exports.WORKFLOW_STATUS = void 0; +exports.WORKFLOW_STATUS = { + IN_PROGRESS: 'in_progress', + QUEUED: 'queued', + REQUESTED: 'requested', + WAITING: 'waiting', + PENDING: 'pending', + COMPLETED: 'completed', + FAILED: 'failed', + CANCELLED: 'cancelled', + SKIPPED: 'skipped', + TIMED_OUT: 'timed_out', +}; +exports.WORKFLOW_ACTIVE_STATUSES = [ + exports.WORKFLOW_STATUS.IN_PROGRESS, + exports.WORKFLOW_STATUS.QUEUED, + exports.WORKFLOW_STATUS.REQUESTED, + exports.WORKFLOW_STATUS.WAITING, + exports.WORKFLOW_STATUS.PENDING, +]; + + /***/ }), /***/ 89040: @@ -70221,6 +71056,64 @@ function githubUsersMatch(left, right) { } +/***/ }), + +/***/ 38572: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.MAX_INACTIVITY_THRESHOLD_HOURS = exports.DEFAULT_INACTIVITY_THRESHOLD_HOURS = void 0; +exports.evaluateIssueInactivity = evaluateIssueInactivity; +/** Default inactivity window used by the scheduled issue-maintenance action. */ +exports.DEFAULT_INACTIVITY_THRESHOLD_HOURS = 168; +/** Maximum supported window (one year) for a finite, operationally useful value. */ +exports.MAX_INACTIVITY_THRESHOLD_HOURS = 8760; +/** + * Decides whether an issue can be closed without depending on GitHub or time + * APIs. GitHub's `updated_at` is treated as the last activity observed by the + * provider; this includes comments and issue metadata changes. + */ +function evaluateIssueInactivity(input) { + if (input.issue.isPullRequest) + return { kind: 'skip', reason: 'pull-request' }; + if (!hasLabel(input.issue.labels, input.waitingLabels)) { + return { kind: 'skip', reason: 'not-waiting' }; + } + if (hasLabel(input.issue.labels, [input.agentActivityLabel])) { + return { kind: 'skip', reason: 'agent-processing' }; + } + if (!Number.isFinite(input.thresholdHours) + || input.thresholdHours <= 0 + || input.thresholdHours > exports.MAX_INACTIVITY_THRESHOLD_HOURS) { + return { kind: 'skip', reason: 'invalid-threshold' }; + } + const updatedAtMilliseconds = Date.parse(input.issue.updatedAt ?? ''); + if (!Number.isFinite(updatedAtMilliseconds)) { + return { kind: 'skip', reason: 'missing-activity-timestamp' }; + } + if (!Number.isFinite(input.nowMilliseconds) || updatedAtMilliseconds > input.nowMilliseconds) { + return { kind: 'skip', reason: 'future-activity' }; + } + const inactiveForMilliseconds = input.nowMilliseconds - updatedAtMilliseconds; + const thresholdMilliseconds = input.thresholdHours * 60 * 60 * 1000; + return inactiveForMilliseconds >= thresholdMilliseconds + ? { kind: 'close', inactiveForMilliseconds } + : { kind: 'skip', reason: 'recent-activity' }; +} +function hasLabel(labels, candidates) { + const normalizedLabels = new Set(labels.map(normalize)); + return candidates.some(candidate => { + const normalizedCandidate = normalize(candidate); + return normalizedCandidate.length > 0 && normalizedLabels.has(normalizedCandidate); + }); +} +function normalize(value) { + return value.trim().toLowerCase(); +} + + /***/ }), /***/ 19879: @@ -70685,7 +71578,7 @@ exports.createRepositoryVariablesClient = createRepositoryVariablesClient; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createIssueTitleClient = exports.createIssueMetadataClient = exports.createIssueLifecycleClient = exports.createIssueLabelsClient = exports.createIssueLabelProvisioningClient = exports.createIssueContentClient = exports.createIssueAssignmentClient = void 0; +exports.createIssueTitleClient = exports.createIssueMetadataClient = exports.createIssueInactivityClient = exports.createIssueLifecycleClient = exports.createIssueLabelsClient = exports.createIssueLabelProvisioningClient = exports.createIssueContentClient = exports.createIssueAssignmentClient = void 0; const octokit_issue_adapters_1 = __nccwpck_require__(77179); const createIssueAssignmentClient = () => new octokit_issue_adapters_1.OctokitIssueAssignmentClientAdapter(); exports.createIssueAssignmentClient = createIssueAssignmentClient; @@ -70697,6 +71590,8 @@ const createIssueLabelsClient = () => new octokit_issue_adapters_1.OctokitIssueL exports.createIssueLabelsClient = createIssueLabelsClient; const createIssueLifecycleClient = () => new octokit_issue_adapters_1.OctokitIssueLifecycleClientAdapter(); exports.createIssueLifecycleClient = createIssueLifecycleClient; +const createIssueInactivityClient = () => new octokit_issue_adapters_1.OctokitIssueInactivityClientAdapter(); +exports.createIssueInactivityClient = createIssueInactivityClient; const createIssueMetadataClient = () => new octokit_issue_adapters_1.OctokitIssueMetadataClientAdapter(); exports.createIssueMetadataClient = createIssueMetadataClient; const createIssueTitleClient = () => new octokit_issue_adapters_1.OctokitIssueTitleClientAdapter(); @@ -70831,6 +71726,25 @@ function createIssueContentCompositionRoot() { } +/***/ }), + +/***/ 74914: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createCloseInactiveIssuesUseCase = createCloseInactiveIssuesUseCase; +const close_inactive_issues_use_case_1 = __nccwpck_require__(84579); +const issue_inactivity_repository_1 = __nccwpck_require__(28868); +const system_issue_inactivity_clock_adapter_1 = __nccwpck_require__(86457); +const github_issue_client_factory_1 = __nccwpck_require__(95883); +const issue_interaction_composition_root_1 = __nccwpck_require__(92503); +function createCloseInactiveIssuesUseCase() { + return new close_inactive_issues_use_case_1.CloseInactiveIssuesUseCase(new issue_inactivity_repository_1.IssueInactivityRepository((0, github_issue_client_factory_1.createIssueInactivityClient)()), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new system_issue_inactivity_clock_adapter_1.SystemIssueInactivityClockAdapter()); +} + + /***/ }), /***/ 92503: @@ -71038,6 +71952,7 @@ const pull_request_use_case_composition_root_1 = __nccwpck_require__(70636); const organization_members_composition_root_1 = __nccwpck_require__(50603); const update_pull_request_description_use_case_1 = __nccwpck_require__(75089); const pull_request_lifecycle_repository_1 = __nccwpck_require__(24189); +const issue_inactivity_composition_root_1 = __nccwpck_require__(74914); function createDetectPotentialProblemsUseCase() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); return new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)(), bugbot.context, bugbot.publication, bugbot.resolution); @@ -71046,7 +71961,7 @@ function createSingleActionUseCaseCompositionRoot() { const repositoryTagPort = new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()); const repositoryReleasePort = new repository_release_publication_repository_1.RepositoryReleasePublicationRepository((0, github_release_client_factory_1.createReleaseClient)()); const issueDescriptionQueryPort = (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(); - return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)())); + return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)()); } function createIssueCommentUseCaseCompositionRoot() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); @@ -71508,7 +72423,7 @@ exports.OctokitOwnerTypeClientAdapter = OctokitOwnerTypeClientAdapter; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.OctokitIssueTitleClientAdapter = exports.OctokitIssueMetadataClientAdapter = exports.OctokitIssueLifecycleClientAdapter = exports.OctokitIssueLabelsClientAdapter = exports.OctokitIssueLabelProvisioningClientAdapter = exports.OctokitIssueContentClientAdapter = exports.OctokitIssueAssignmentClientAdapter = void 0; +exports.OctokitIssueTitleClientAdapter = exports.OctokitIssueMetadataClientAdapter = exports.OctokitIssueInactivityClientAdapter = exports.OctokitIssueLifecycleClientAdapter = exports.OctokitIssueLabelsClientAdapter = exports.OctokitIssueLabelProvisioningClientAdapter = exports.OctokitIssueContentClientAdapter = exports.OctokitIssueAssignmentClientAdapter = void 0; const octokit_client_resolver_1 = __nccwpck_require__(54047); class OctokitIssueAssignmentClientAdapter { getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } @@ -71530,6 +72445,10 @@ class OctokitIssueLifecycleClientAdapter { getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } } exports.OctokitIssueLifecycleClientAdapter = OctokitIssueLifecycleClientAdapter; +class OctokitIssueInactivityClientAdapter { + getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } +} +exports.OctokitIssueInactivityClientAdapter = OctokitIssueInactivityClientAdapter; class OctokitIssueMetadataClientAdapter { getClient(token) { return (0, octokit_client_resolver_1.getOctokitClient)(token); } } @@ -71760,6 +72679,23 @@ class SetupWorkspaceAdapter { exports.SetupWorkspaceAdapter = SetupWorkspaceAdapter; +/***/ }), + +/***/ 86457: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SystemIssueInactivityClockAdapter = void 0; +class SystemIssueInactivityClockAdapter { + nowMilliseconds() { + return Date.now(); + } +} +exports.SystemIssueInactivityClockAdapter = SystemIssueInactivityClockAdapter; + + /***/ }), /***/ 32679: @@ -72720,429 +73656,6 @@ function stripTrailingCommentWatermarks(comment) { } -/***/ }), - -/***/ 15415: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.PROMPTS = exports.BUGBOT_MIN_SEVERITY = exports.BUGBOT_MAX_COMMENTS = exports.BUGBOT_MARKER_PREFIX = exports.ACTIONS = exports.ERRORS = exports.INPUT_KEYS = exports.WORKFLOW_ACTIVE_STATUSES = exports.WORKFLOW_STATUS = exports.DEFAULT_IMAGE_CONFIG = exports.AGENT_REQUEST_TIMEOUT_MS = exports.TITLE = void 0; -exports.TITLE = 'Copilot'; -/** Maximum time allowed for one external agent CLI request. */ -exports.AGENT_REQUEST_TIMEOUT_MS = 900000; -exports.DEFAULT_IMAGE_CONFIG = { - issue: { - automatic: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp" - ], - feature: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", - ], - bugfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" - ], - hotfix: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp" - ], - release: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", - ], - docs: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", - ], - chore: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", - ], - }, - pullRequest: { - automatic: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYzRsNGFicndqMXgzMTVwdnhpeXNyZGsydXVxamV4eGxndWhna291OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ktcUyw6mBlMVa/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjkyeWVubngzM28xODFrbXZ4Nng3Y2hubmM4cXJqNGpic3Bheml0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/M11UVCRrc0LUk/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenQwNDJmZnZraDBzNXBoNjUwZjEzMzFlanMxcHVodmF4b3l3bDl2biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrdUjl6N99nLq/200.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - ], - feature: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMm5iZHJydTJ4NGticXdxd3ZxYnZqNXdvaDQwOHdtb3o5NTRhdnRhOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LSX49vHf7JHGyGjrC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYmc4YWplZWs0Y2c3ZXNtbGpwZnQzdWpncmNjNXpodjg3MHdtbnJ5NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OMK7LRBedcnhm/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHBrYXpmd2poeGU5cWswbjRqNmJlZ2U2dWc0ejVpY3RpcXVuYTY3dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/llKJGxQ1ESmac/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnFleXV0MXZteGN6c2s2b3R3ZGc2cWY1aXB0Y3ZzNmpvZHhyNDVmNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10FwycrnAkpshW/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHo0MjIzaGIycTRmeWFwZmp6bGExczJicXcyZTQxemsxaTY1b3V1NiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/QKkV58ufpV4ksJ1Okh/giphy.gif", - ], - bugfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExazc3OWszenA5c2FlemE3a25oNnlmZDBra3liMWRqMW82NzM2b2FveCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xPGkOAdiIO3Is/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3liaGF2NzI3bzM1YjRmdHFsaGdyenp4b3o3M3dqM3F0bGN5MHZtNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/npUpB306c3EStRK6qP/200.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWh6d3Nld3E0MTF1eTk2YXFibnI3MTBhbGtpamJiemRwejl3YmkzMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gU25raLP4pUu4/giphy.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", - ], - hotfix: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmozN3plMWNiYjZoemh6N2RmeTB1MG9ieHlqYTJsb3BrZmNoY3h0dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/stv1Dliu5TrMs/giphy.webp", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2R0cjNxbXBjZjRjNmg4NmN3MGlhazVkNHJsaDkxMHZkY2hweGRtZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/pCU4bC7kC6sxy/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExenkyZTc3aDlweWl0MnI0cXJsZGptY3g0bzE2NTY1aWMyaHd4Y201ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dbtDDSvWErdf2/giphy.webp", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM25ndGd2d3Uya3g3dnlnenJ1bjh0Y2NtNHdwZHY3Mjh2NnBmZDJpbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2xF8gHUf085aNyyAQR/200.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdjU3bHdsc3FtamlyazBlbWppNHc3MTV3MW4xdHd2cWo4b2tzbTkwcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1EghTrigJJhq8/200.webp", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmM1OWR0cnk5eXI0dXpoNWRzbmVseTVyd2l3MzdrOHZueHJ6bjhjMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/12yjKJaLB7DuG4/giphy.webp", - ], - release: [ - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2NxcHEzam92enRtd29xc21pMHhmbHozMWljamF1cmt4cjhwZTI0ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/PApUm1HPVYlDNLoMmr/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXU4dnhwOWVqZzc4NXVsdTY3c2I4Mm9lOHF1c253MDJya25zNXU0ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dxn6fRlTIShoeBr69N/giphy.webp", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXN2bjJob3pxazE2NDJhbGE3ZWY5d2dzbDM4czgwZnA4ejlxY3ZqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D37RaHngWP7ZmmsEt/giphy.webp", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnI0YTM2N2hwamd2dXYwNmN2MjRpYXIyN203cnNpbW13YjNhZGRhdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/LYWPXVUNz30ze/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW1jZ3F4ZGRwMWkyc3ZocHJ3aXhyb2FuZGppcnMyMWtsYXpjbDY2ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tXLpxypfSXvUc/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHRianpoOW51MzZ4Yjk3MmNpbmdseTJlb3o3dWVpYzJpazc5ZHNoayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/b85mPT4Usz7fq/giphy.gif", - ], - docs: [ - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGRpZHJqYzRvZ25xcjR3ZXcwbzVudXF2Z2hsaHoyc2g1ZjZuam81YiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eDArHBLT4aATKEKtCd/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2NubXR1b2M1dDQ2Z2UxYmk5bzltbHdudWI1emVzOGFlbDNsOGU1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wpgYasZ0tBrP4lCgS3/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmEyNzc3M2V0anp4d2JtOTJuMTZ2dXNnMmEyN3A4MmE0ZGpiaDhnNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orifaQEOagjYJ1EXe/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjUyenc2eG5pZ3NjYzcyZXg2dDFndm5qZHRqMHk5amNoYjhhNnNvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7E8lI6TkLrvvAcPXso/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWFxcXZ3MTMxM3Bjd2IwNG43ZDJjdndreXNmdTVvZ2g3Z2Q4NjczMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3tJdi9wQQ10BD2H47g/giphy.gif", - "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFrejZmaHQ2Z2o1Y3B2MDl6cmU5bzNybG84eXFrYjBjZjV0dGFpeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fsXOS3oBboiYf6fSsY/giphy.gif", - "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHdhOHRianU1YmtrNHE0c2R2M2I2MTBzNnZhdnBrMW5ueG02eHF6OSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieOEBYMAwTClHqM/giphy.gif", - ], - chore: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFtNXY0ZXdmdGxkdno2Nm5odGk3Nzd3aTRuYnJtbDA4MXIxdHFhdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/10zsjaH4g0GgmY/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZG1sNXB6eTZvdDNtNzJwNXVxenNjendwaGgxb2xzNWI1dGNpdTVmZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NHHYRm7mAUQ6Y/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHd4bDJrc216YWpicDQ5emczdWF3bTk0dXYzeGQ4ajg2a3IyYjV6diZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FHEjBpiqMwSuA/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3d5b2U1Z3Jic3AxY2llYjQwNW5wODFpNWp5NHY0dGV5Z2cxdThkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kLZNLNqUZ6bC0/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTNpZ2w0c3NrMmc0cmZobTd2eTM3YTRlM2lnbWpoZDUzNnRjdnNmZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/NV4cSrRYXXwfUcYnua/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmFzZHNuODg0dDRheGt0aGU2bjVvd2xiNDI1bWFmYTVsbHJ2eHI2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XaAbmtzzz35IgW3Ntn/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExYWM2OHkzYmNkajZxa204Njg0bmQzaWp1M3NobnJjbWxyYWJrbDNnciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OiwOPq0fFqqyainyMu/giphy.gif", - ], - }, - commit: { - automatic: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - feature: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - bugfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - hotfix: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - release: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - docs: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ], - chore: [ - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdWp2OGJ5ZmczaGhiMmVxdjRxMWZnYnRrNW5uemlmd2Ewam1nNGd0aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/2XflxzEtr4EPIEzioLu/giphy.gif", - "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExaTkzeTFveHd6N3Fubm8yZDlpYTVuMnp0bm1rODQyZDdpbTF4YzAxaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/n2IPMYMthV0m4/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3BmNXV1YzZod2NkYjZ3aTE1Z3BwMWJ0ZG9uMXN0bm5pbDQ4ajBvaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3WxRbhsvQjYw8/giphy.gif", - "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWs5YXEyajhoNWI1aHdxeHNwcmt2czY2NW1mNjZrbnViYm9reXJsZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/loLqo6AzjUcMdjS1Jj/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHh5MndyMzBmY3c3bDRxeGhpanF2ZjIycGpmbzlkMDV5cDJkeXhjMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3orieQDBZVlki2mJLW/giphy.gif", - "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGdkaHFsMTlzM2ZuY3R5ZXJpZmo3cHRqZWJieXVlOHQwc2F3eGVrdSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/tELuxgGsoL62ihEtQs/giphy.gif", - ] - } -}; -exports.WORKFLOW_STATUS = { - IN_PROGRESS: 'in_progress', - QUEUED: 'queued', - REQUESTED: 'requested', - WAITING: 'waiting', - PENDING: 'pending', - COMPLETED: 'completed', - FAILED: 'failed', - CANCELLED: 'cancelled', - SKIPPED: 'skipped', - TIMED_OUT: 'timed_out', -}; -exports.WORKFLOW_ACTIVE_STATUSES = [ - exports.WORKFLOW_STATUS.IN_PROGRESS, - exports.WORKFLOW_STATUS.QUEUED, - exports.WORKFLOW_STATUS.REQUESTED, - exports.WORKFLOW_STATUS.WAITING, - exports.WORKFLOW_STATUS.PENDING, -]; -exports.INPUT_KEYS = { - // Debug - DEBUG: 'debug', - // Welcome - WELCOME_TITLE: 'welcome-title', - WELCOME_MESSAGES: 'welcome-messages', - // Single action - SINGLE_ACTION: 'single-action', - SINGLE_ACTION_ISSUE: 'single-action-issue', - SINGLE_ACTION_VERSION: 'single-action-version', - SINGLE_ACTION_TITLE: 'single-action-title', - SINGLE_ACTION_CHANGELOG: 'single-action-changelog', - // Tokens - TOKEN: 'token', - QUEUE_GATE_ONLY: 'queue-gate-only', - // Agent selection - AGENT_PROVIDER: 'agent-provider', - AGENT_MODEL_PROVIDER: 'agent-model-provider', - AGENT_EFFORT: 'agent-effort', - AGENT_MODEL: 'agent-model', - AGENT_COMMAND: 'agent-command', - FINDINGS_PROVIDER: 'findings-provider', - FINDINGS_MODEL_PROVIDER: 'findings-model-provider', - FINDINGS_EFFORT: 'findings-effort', - FINDINGS_MODEL: 'findings-model', - FINDINGS_COMMAND: 'findings-command', - FIXER_PROVIDER: 'fixer-provider', - FIXER_MODEL_PROVIDER: 'fixer-model-provider', - FIXER_EFFORT: 'fixer-effort', - FIXER_MODEL: 'fixer-model', - FIXER_COMMAND: 'fixer-command', - PLANNER_PROVIDER: 'planner-provider', - PLANNER_MODEL_PROVIDER: 'planner-model-provider', - PLANNER_EFFORT: 'planner-effort', - PLANNER_MODEL: 'planner-model', - PLANNER_COMMAND: 'planner-command', - REVIEWER_PROVIDER: 'reviewer-provider', - REVIEWER_MODEL_PROVIDER: 'reviewer-model-provider', - REVIEWER_EFFORT: 'reviewer-effort', - REVIEWER_MODEL: 'reviewer-model', - REVIEWER_COMMAND: 'reviewer-command', - TESTER_PROVIDER: 'tester-provider', - TESTER_MODEL_PROVIDER: 'tester-model-provider', - TESTER_EFFORT: 'tester-effort', - TESTER_MODEL: 'tester-model', - TESTER_COMMAND: 'tester-command', - RELEASE_PROVIDER: 'release-provider', - RELEASE_MODEL_PROVIDER: 'release-model-provider', - RELEASE_EFFORT: 'release-effort', - RELEASE_MODEL: 'release-model', - RELEASE_COMMAND: 'release-command', - // AI configuration - AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', - AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', - AI_MEMBERS_ONLY: 'ai-members-only', - AI_IGNORE_FILES: 'ai-ignore-files', - AI_INCLUDE_REASONING: 'ai-include-reasoning', - BUGBOT_SEVERITY: 'bugbot-severity', - BUGBOT_COMMENT_LIMIT: 'bugbot-comment-limit', - BUGBOT_FIX_VERIFY_COMMANDS: 'bugbot-fix-verify-commands', - // Projects - PROJECT_IDS: 'project-ids', - PROJECT_COLUMN_ISSUE_CREATED: 'project-column-issue-created', - PROJECT_COLUMN_PULL_REQUEST_CREATED: 'project-column-pull-request-created', - PROJECT_COLUMN_ISSUE_IN_PROGRESS: 'project-column-issue-in-progress', - PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: 'project-column-pull-request-in-progress', - // Images - IMAGES_ON_ISSUE: 'images-on-issue', - IMAGES_ON_PULL_REQUEST: 'images-on-pull-request', - IMAGES_ON_COMMIT: 'images-on-commit', - IMAGES_ISSUE_AUTOMATIC: 'images-issue-automatic', - IMAGES_ISSUE_FEATURE: 'images-issue-feature', - IMAGES_ISSUE_BUGFIX: 'images-issue-bugfix', - IMAGES_ISSUE_DOCS: 'images-issue-docs', - IMAGES_ISSUE_CHORE: 'images-issue-chore', - IMAGES_ISSUE_RELEASE: 'images-issue-release', - IMAGES_ISSUE_HOTFIX: 'images-issue-hotfix', - IMAGES_PULL_REQUEST_AUTOMATIC: 'images-pull-request-automatic', - IMAGES_PULL_REQUEST_FEATURE: 'images-pull-request-feature', - IMAGES_PULL_REQUEST_BUGFIX: 'images-pull-request-bugfix', - IMAGES_PULL_REQUEST_RELEASE: 'images-pull-request-release', - IMAGES_PULL_REQUEST_HOTFIX: 'images-pull-request-hotfix', - IMAGES_PULL_REQUEST_DOCS: 'images-pull-request-docs', - IMAGES_PULL_REQUEST_CHORE: 'images-pull-request-chore', - IMAGES_COMMIT_AUTOMATIC: 'images-commit-automatic', - IMAGES_COMMIT_FEATURE: 'images-commit-feature', - IMAGES_COMMIT_BUGFIX: 'images-commit-bugfix', - IMAGES_COMMIT_RELEASE: 'images-commit-release', - IMAGES_COMMIT_HOTFIX: 'images-commit-hotfix', - IMAGES_COMMIT_DOCS: 'images-commit-docs', - IMAGES_COMMIT_CHORE: 'images-commit-chore', - // Workflows - RELEASE_WORKFLOW: 'release-workflow', - HOTFIX_WORKFLOW: 'hotfix-workflow', - // Emoji - EMOJI_LABELED_TITLE: 'emoji-labeled-title', - BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', - // Labels - BRANCH_MANAGEMENT_LAUNCHER_LABEL: 'branch-management-launcher-label', - BUGFIX_LABEL: 'bugfix-label', - BUG_LABEL: 'bug-label', - HOTFIX_LABEL: 'hotfix-label', - ENHANCEMENT_LABEL: 'enhancement-label', - FEATURE_LABEL: 'feature-label', - RELEASE_LABEL: 'release-label', - QUESTION_LABEL: 'question-label', - HELP_LABEL: 'help-label', - DEPLOY_LABEL: 'deploy-label', - DEPLOYED_LABEL: 'deployed-label', - DOCS_LABEL: 'docs-label', - DOCUMENTATION_LABEL: 'documentation-label', - CHORE_LABEL: 'chore-label', - MAINTENANCE_LABEL: 'maintenance-label', - PRIORITY_HIGH_LABEL: 'priority-high-label', - PRIORITY_MEDIUM_LABEL: 'priority-medium-label', - PRIORITY_LOW_LABEL: 'priority-low-label', - PRIORITY_NONE_LABEL: 'priority-none-label', - SIZE_XXL_LABEL: 'size-xxl-label', - SIZE_XL_LABEL: 'size-xl-label', - SIZE_L_LABEL: 'size-l-label', - SIZE_M_LABEL: 'size-m-label', - SIZE_S_LABEL: 'size-s-label', - SIZE_XS_LABEL: 'size-xs-label', - // Lifecycle label inputs - STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', - STATE_PLANNED_LABEL: 'state-planned-label', - STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', - STATE_REVIEWING_LABEL: 'state-reviewing-label', - STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', - STATE_VERIFIED_LABEL: 'state-verified-label', - STATE_READY_LABEL: 'state-ready-label', - STATE_BLOCKED_LABEL: 'state-blocked-label', - STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', - STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', - // Issue Types - ISSUE_TYPE_BUG: 'issue-type-bug', - ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', - ISSUE_TYPE_BUG_COLOR: 'issue-type-bug-color', - ISSUE_TYPE_HOTFIX: 'issue-type-hotfix', - ISSUE_TYPE_HOTFIX_DESCRIPTION: 'issue-type-hotfix-description', - ISSUE_TYPE_HOTFIX_COLOR: 'issue-type-hotfix-color', - ISSUE_TYPE_FEATURE: 'issue-type-feature', - ISSUE_TYPE_FEATURE_DESCRIPTION: 'issue-type-feature-description', - ISSUE_TYPE_FEATURE_COLOR: 'issue-type-feature-color', - ISSUE_TYPE_DOCUMENTATION: 'issue-type-documentation', - ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: 'issue-type-documentation-description', - ISSUE_TYPE_DOCUMENTATION_COLOR: 'issue-type-documentation-color', - ISSUE_TYPE_MAINTENANCE: 'issue-type-maintenance', - ISSUE_TYPE_MAINTENANCE_DESCRIPTION: 'issue-type-maintenance-description', - ISSUE_TYPE_MAINTENANCE_COLOR: 'issue-type-maintenance-color', - ISSUE_TYPE_RELEASE: 'issue-type-release', - ISSUE_TYPE_RELEASE_DESCRIPTION: 'issue-type-release-description', - ISSUE_TYPE_RELEASE_COLOR: 'issue-type-release-color', - ISSUE_TYPE_QUESTION: 'issue-type-question', - ISSUE_TYPE_QUESTION_DESCRIPTION: 'issue-type-question-description', - ISSUE_TYPE_QUESTION_COLOR: 'issue-type-question-color', - ISSUE_TYPE_HELP: 'issue-type-help', - ISSUE_TYPE_HELP_DESCRIPTION: 'issue-type-help-description', - ISSUE_TYPE_HELP_COLOR: 'issue-type-help-color', - ISSUE_TYPE_TASK: 'issue-type-task', - ISSUE_TYPE_TASK_DESCRIPTION: 'issue-type-task-description', - ISSUE_TYPE_TASK_COLOR: 'issue-type-task-color', - // Locale - ISSUES_LOCALE: 'issues-locale', - PULL_REQUESTS_LOCALE: 'pull-requests-locale', - // Size Thresholds - SIZE_XXL_THRESHOLD_LINES: 'size-xxl-threshold-lines', - SIZE_XXL_THRESHOLD_FILES: 'size-xxl-threshold-files', - SIZE_XXL_THRESHOLD_COMMITS: 'size-xxl-threshold-commits', - SIZE_XL_THRESHOLD_LINES: 'size-xl-threshold-lines', - SIZE_XL_THRESHOLD_FILES: 'size-xl-threshold-files', - SIZE_XL_THRESHOLD_COMMITS: 'size-xl-threshold-commits', - SIZE_L_THRESHOLD_LINES: 'size-l-threshold-lines', - SIZE_L_THRESHOLD_FILES: 'size-l-threshold-files', - SIZE_L_THRESHOLD_COMMITS: 'size-l-threshold-commits', - SIZE_M_THRESHOLD_LINES: 'size-m-threshold-lines', - SIZE_M_THRESHOLD_FILES: 'size-m-threshold-files', - SIZE_M_THRESHOLD_COMMITS: 'size-m-threshold-commits', - SIZE_S_THRESHOLD_LINES: 'size-s-threshold-lines', - SIZE_S_THRESHOLD_FILES: 'size-s-threshold-files', - SIZE_S_THRESHOLD_COMMITS: 'size-s-threshold-commits', - SIZE_XS_THRESHOLD_LINES: 'size-xs-threshold-lines', - SIZE_XS_THRESHOLD_FILES: 'size-xs-threshold-files', - SIZE_XS_THRESHOLD_COMMITS: 'size-xs-threshold-commits', - // Branches - MAIN_BRANCH: 'main-branch', - DEVELOPMENT_BRANCH: 'development-branch', - FEATURE_TREE: 'feature-tree', - BUGFIX_TREE: 'bugfix-tree', - HOTFIX_TREE: 'hotfix-tree', - RELEASE_TREE: 'release-tree', - DOCS_TREE: 'docs-tree', - CHORE_TREE: 'chore-tree', - // Commit - COMMIT_PREFIX_TRANSFORMS: 'commit-prefix-transforms', - // Issue - BRANCH_MANAGEMENT_ALWAYS: 'branch-management-always', - REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', - DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', - // Pull Request - PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', - PULL_REQUEST_DESIRED_REVIEWERS_COUNT: 'desired-reviewers-count', - PULL_REQUEST_MERGE_TIMEOUT: 'merge-timeout', -}; -exports.ERRORS = { - GIT_REPOSITORY_NOT_FOUND: '❌ Git repository not found' -}; -var action_types_1 = __nccwpck_require__(19625); -Object.defineProperty(exports, "ACTIONS", ({ enumerable: true, get: function () { return action_types_1.ACTIONS; } })); -/** Hidden HTML comment prefix for bugbot findings (issue/PR comments). Format: */ -exports.BUGBOT_MARKER_PREFIX = 'copilot-bugbot'; -/** Max number of individual bugbot comments to create per issue/PR. Excess findings get one summary comment suggesting to review locally. */ -exports.BUGBOT_MAX_COMMENTS = 20; -/** Minimum severity to publish (findings below this are dropped). Order: high > medium > low > info. */ -exports.BUGBOT_MIN_SEVERITY = 'low'; -exports.PROMPTS = {}; - - /***/ }), /***/ 92816: @@ -73595,6 +74108,7 @@ function copySetupFiles(cwd, setupDirOverride, features, options = {}) { 'hotfix_workflow.yml': 'hotfix', 'agent-cli-provisioning.yml': 'agentProvisioning', 'copilot_credential_health.yml': 'credentialHealth', + 'copilot_close_inactive_issues.yml': 'inactiveIssueClosure', }; const approvedWorkflowFiles = new Set(options.approvedWorkflowFiles ?? []); const backupDirectory = options.updateExistingWorkflows ? path.join(cwd, '.copilot', 'setup-backups', new Date().toISOString().replace(/[:.]/g, '-')) : undefined; @@ -73629,6 +74143,7 @@ function compareSetupWorkflows(cwd, features, setupDirOverride) { 'hotfix_workflow.yml': 'hotfix', 'agent-cli-provisioning.yml': 'agentProvisioning', 'copilot_credential_health.yml': 'credentialHealth', + 'copilot_close_inactive_issues.yml': 'inactiveIssueClosure', }; const sourceDirectory = path.join(setupDir, 'workflows'); if (!fs.existsSync(sourceDirectory)) diff --git a/build/github_action/src/actions/default_image_config.d.ts b/build/github_action/src/actions/default_image_config.d.ts new file mode 100644 index 00000000..8d656ad3 --- /dev/null +++ b/build/github_action/src/actions/default_image_config.d.ts @@ -0,0 +1,30 @@ +/** Default illustration URLs used when an action does not receive custom images. */ +export declare const DEFAULT_IMAGE_CONFIG: { + issue: { + automatic: string[]; + feature: string[]; + bugfix: string[]; + hotfix: string[]; + release: string[]; + docs: string[]; + chore: string[]; + }; + pullRequest: { + automatic: string[]; + feature: string[]; + bugfix: string[]; + hotfix: string[]; + release: string[]; + docs: string[]; + chore: string[]; + }; + commit: { + automatic: string[]; + feature: string[]; + bugfix: string[]; + hotfix: string[]; + release: string[]; + docs: string[]; + chore: string[]; + }; +}; diff --git a/build/github_action/src/actions/image_configuration_builder.d.ts b/build/github_action/src/actions/image_configuration_builder.d.ts index e815dd30..e17cb825 100644 --- a/build/github_action/src/actions/image_configuration_builder.d.ts +++ b/build/github_action/src/actions/image_configuration_builder.d.ts @@ -1,4 +1,4 @@ -import { DEFAULT_IMAGE_CONFIG } from '../utils/constants'; +import { DEFAULT_IMAGE_CONFIG } from './default_image_config'; export type ImageConfigurationReader = (key: string) => unknown; type ImageGroup = keyof typeof DEFAULT_IMAGE_CONFIG; type ImageVariant = keyof (typeof DEFAULT_IMAGE_CONFIG)[ImageGroup]; diff --git a/build/github_action/src/actions/local_action_configuration.d.ts b/build/github_action/src/actions/local_action_configuration.d.ts index f6ab15af..93a04530 100644 --- a/build/github_action/src/actions/local_action_configuration.d.ts +++ b/build/github_action/src/actions/local_action_configuration.d.ts @@ -134,6 +134,7 @@ export declare function buildLocalActionConfiguration(additionalParams: ActionIn singleActionVersion: string; singleActionTitle: string; singleActionChangelog: string; + inactivityThresholdHours: number; token: string; }>; export type LocalActionConfiguration = Awaited>; diff --git a/build/github_action/src/actions/local_action_configuration_sections.d.ts b/build/github_action/src/actions/local_action_configuration_sections.d.ts index 7b0d3a20..aad4e03d 100644 --- a/build/github_action/src/actions/local_action_configuration_sections.d.ts +++ b/build/github_action/src/actions/local_action_configuration_sections.d.ts @@ -12,6 +12,7 @@ export declare function readLocalCoreConfiguration(additionalParams: ActionInput singleActionVersion: string; singleActionTitle: string; singleActionChangelog: string; + inactivityThresholdHours: number; token: string; }; export declare function readLocalAgentConfiguration(additionalParams: ActionInputValues, actionInputs: LocalActionInputs): { diff --git a/build/github_action/src/application/contracts/input_keys.d.ts b/build/github_action/src/application/contracts/input_keys.d.ts new file mode 100644 index 00000000..af1b6a5e --- /dev/null +++ b/build/github_action/src/application/contracts/input_keys.d.ts @@ -0,0 +1,187 @@ +/** Canonical action and CLI input vocabulary shared by input mappers. */ +export declare const INPUT_KEYS: { + readonly DEBUG: "debug"; + readonly WELCOME_TITLE: "welcome-title"; + readonly WELCOME_MESSAGES: "welcome-messages"; + readonly SINGLE_ACTION: "single-action"; + readonly SINGLE_ACTION_ISSUE: "single-action-issue"; + readonly SINGLE_ACTION_VERSION: "single-action-version"; + readonly SINGLE_ACTION_TITLE: "single-action-title"; + readonly SINGLE_ACTION_CHANGELOG: "single-action-changelog"; + readonly INACTIVITY_THRESHOLD_HOURS: "inactivity-threshold-hours"; + readonly TOKEN: "token"; + readonly QUEUE_GATE_ONLY: "queue-gate-only"; + readonly AGENT_PROVIDER: "agent-provider"; + readonly AGENT_MODEL_PROVIDER: "agent-model-provider"; + readonly AGENT_EFFORT: "agent-effort"; + readonly AGENT_MODEL: "agent-model"; + readonly AGENT_COMMAND: "agent-command"; + readonly FINDINGS_PROVIDER: "findings-provider"; + readonly FINDINGS_MODEL_PROVIDER: "findings-model-provider"; + readonly FINDINGS_EFFORT: "findings-effort"; + readonly FINDINGS_MODEL: "findings-model"; + readonly FINDINGS_COMMAND: "findings-command"; + readonly FIXER_PROVIDER: "fixer-provider"; + readonly FIXER_MODEL_PROVIDER: "fixer-model-provider"; + readonly FIXER_EFFORT: "fixer-effort"; + readonly FIXER_MODEL: "fixer-model"; + readonly FIXER_COMMAND: "fixer-command"; + readonly PLANNER_PROVIDER: "planner-provider"; + readonly PLANNER_MODEL_PROVIDER: "planner-model-provider"; + readonly PLANNER_EFFORT: "planner-effort"; + readonly PLANNER_MODEL: "planner-model"; + readonly PLANNER_COMMAND: "planner-command"; + readonly REVIEWER_PROVIDER: "reviewer-provider"; + readonly REVIEWER_MODEL_PROVIDER: "reviewer-model-provider"; + readonly REVIEWER_EFFORT: "reviewer-effort"; + readonly REVIEWER_MODEL: "reviewer-model"; + readonly REVIEWER_COMMAND: "reviewer-command"; + readonly TESTER_PROVIDER: "tester-provider"; + readonly TESTER_MODEL_PROVIDER: "tester-model-provider"; + readonly TESTER_EFFORT: "tester-effort"; + readonly TESTER_MODEL: "tester-model"; + readonly TESTER_COMMAND: "tester-command"; + readonly RELEASE_PROVIDER: "release-provider"; + readonly RELEASE_MODEL_PROVIDER: "release-model-provider"; + readonly RELEASE_EFFORT: "release-effort"; + readonly RELEASE_MODEL: "release-model"; + readonly RELEASE_COMMAND: "release-command"; + readonly AI_PULL_REQUEST_DESCRIPTION: "ai-pull-request-description"; + readonly AI_PULL_REQUEST_DESCRIPTION_MODE: "ai-pull-request-description-mode"; + readonly AI_MEMBERS_ONLY: "ai-members-only"; + readonly AI_IGNORE_FILES: "ai-ignore-files"; + readonly AI_INCLUDE_REASONING: "ai-include-reasoning"; + readonly BUGBOT_SEVERITY: "bugbot-severity"; + readonly BUGBOT_COMMENT_LIMIT: "bugbot-comment-limit"; + readonly BUGBOT_FIX_VERIFY_COMMANDS: "bugbot-fix-verify-commands"; + readonly PROJECT_IDS: "project-ids"; + readonly PROJECT_COLUMN_ISSUE_CREATED: "project-column-issue-created"; + readonly PROJECT_COLUMN_PULL_REQUEST_CREATED: "project-column-pull-request-created"; + readonly PROJECT_COLUMN_ISSUE_IN_PROGRESS: "project-column-issue-in-progress"; + readonly PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: "project-column-pull-request-in-progress"; + readonly IMAGES_ON_ISSUE: "images-on-issue"; + readonly IMAGES_ON_PULL_REQUEST: "images-on-pull-request"; + readonly IMAGES_ON_COMMIT: "images-on-commit"; + readonly IMAGES_ISSUE_AUTOMATIC: "images-issue-automatic"; + readonly IMAGES_ISSUE_FEATURE: "images-issue-feature"; + readonly IMAGES_ISSUE_BUGFIX: "images-issue-bugfix"; + readonly IMAGES_ISSUE_DOCS: "images-issue-docs"; + readonly IMAGES_ISSUE_CHORE: "images-issue-chore"; + readonly IMAGES_ISSUE_RELEASE: "images-issue-release"; + readonly IMAGES_ISSUE_HOTFIX: "images-issue-hotfix"; + readonly IMAGES_PULL_REQUEST_AUTOMATIC: "images-pull-request-automatic"; + readonly IMAGES_PULL_REQUEST_FEATURE: "images-pull-request-feature"; + readonly IMAGES_PULL_REQUEST_BUGFIX: "images-pull-request-bugfix"; + readonly IMAGES_PULL_REQUEST_RELEASE: "images-pull-request-release"; + readonly IMAGES_PULL_REQUEST_HOTFIX: "images-pull-request-hotfix"; + readonly IMAGES_PULL_REQUEST_DOCS: "images-pull-request-docs"; + readonly IMAGES_PULL_REQUEST_CHORE: "images-pull-request-chore"; + readonly IMAGES_COMMIT_AUTOMATIC: "images-commit-automatic"; + readonly IMAGES_COMMIT_FEATURE: "images-commit-feature"; + readonly IMAGES_COMMIT_BUGFIX: "images-commit-bugfix"; + readonly IMAGES_COMMIT_RELEASE: "images-commit-release"; + readonly IMAGES_COMMIT_HOTFIX: "images-commit-hotfix"; + readonly IMAGES_COMMIT_DOCS: "images-commit-docs"; + readonly IMAGES_COMMIT_CHORE: "images-commit-chore"; + readonly RELEASE_WORKFLOW: "release-workflow"; + readonly HOTFIX_WORKFLOW: "hotfix-workflow"; + readonly EMOJI_LABELED_TITLE: "emoji-labeled-title"; + readonly BRANCH_MANAGEMENT_EMOJI: "branch-management-emoji"; + readonly BRANCH_MANAGEMENT_LAUNCHER_LABEL: "branch-management-launcher-label"; + readonly BUGFIX_LABEL: "bugfix-label"; + readonly BUG_LABEL: "bug-label"; + readonly HOTFIX_LABEL: "hotfix-label"; + readonly ENHANCEMENT_LABEL: "enhancement-label"; + readonly FEATURE_LABEL: "feature-label"; + readonly RELEASE_LABEL: "release-label"; + readonly QUESTION_LABEL: "question-label"; + readonly HELP_LABEL: "help-label"; + readonly DEPLOY_LABEL: "deploy-label"; + readonly DEPLOYED_LABEL: "deployed-label"; + readonly DOCS_LABEL: "docs-label"; + readonly DOCUMENTATION_LABEL: "documentation-label"; + readonly CHORE_LABEL: "chore-label"; + readonly MAINTENANCE_LABEL: "maintenance-label"; + readonly PRIORITY_HIGH_LABEL: "priority-high-label"; + readonly PRIORITY_MEDIUM_LABEL: "priority-medium-label"; + readonly PRIORITY_LOW_LABEL: "priority-low-label"; + readonly PRIORITY_NONE_LABEL: "priority-none-label"; + readonly SIZE_XXL_LABEL: "size-xxl-label"; + readonly SIZE_XL_LABEL: "size-xl-label"; + readonly SIZE_L_LABEL: "size-l-label"; + readonly SIZE_M_LABEL: "size-m-label"; + readonly SIZE_S_LABEL: "size-s-label"; + readonly SIZE_XS_LABEL: "size-xs-label"; + readonly STATE_AI_PROCESSING_LABEL: "state-ai-processing-label"; + readonly STATE_PLANNED_LABEL: "state-planned-label"; + readonly STATE_IN_PROGRESS_LABEL: "state-in-progress-label"; + readonly STATE_REVIEWING_LABEL: "state-reviewing-label"; + readonly STATE_CHANGES_REQUESTED_LABEL: "state-changes-requested-label"; + readonly STATE_VERIFIED_LABEL: "state-verified-label"; + readonly STATE_READY_LABEL: "state-ready-label"; + readonly STATE_BLOCKED_LABEL: "state-blocked-label"; + readonly STATE_AWAITING_MAINTAINER_LABEL: "state-awaiting-maintainer-label"; + readonly STATE_AWAITING_ISSUE_AUTHOR_LABEL: "state-awaiting-issue-author-label"; + readonly ISSUE_TYPE_BUG: "issue-type-bug"; + readonly ISSUE_TYPE_BUG_DESCRIPTION: "issue-type-bug-description"; + readonly ISSUE_TYPE_BUG_COLOR: "issue-type-bug-color"; + readonly ISSUE_TYPE_HOTFIX: "issue-type-hotfix"; + readonly ISSUE_TYPE_HOTFIX_DESCRIPTION: "issue-type-hotfix-description"; + readonly ISSUE_TYPE_HOTFIX_COLOR: "issue-type-hotfix-color"; + readonly ISSUE_TYPE_FEATURE: "issue-type-feature"; + readonly ISSUE_TYPE_FEATURE_DESCRIPTION: "issue-type-feature-description"; + readonly ISSUE_TYPE_FEATURE_COLOR: "issue-type-feature-color"; + readonly ISSUE_TYPE_DOCUMENTATION: "issue-type-documentation"; + readonly ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: "issue-type-documentation-description"; + readonly ISSUE_TYPE_DOCUMENTATION_COLOR: "issue-type-documentation-color"; + readonly ISSUE_TYPE_MAINTENANCE: "issue-type-maintenance"; + readonly ISSUE_TYPE_MAINTENANCE_DESCRIPTION: "issue-type-maintenance-description"; + readonly ISSUE_TYPE_MAINTENANCE_COLOR: "issue-type-maintenance-color"; + readonly ISSUE_TYPE_RELEASE: "issue-type-release"; + readonly ISSUE_TYPE_RELEASE_DESCRIPTION: "issue-type-release-description"; + readonly ISSUE_TYPE_RELEASE_COLOR: "issue-type-release-color"; + readonly ISSUE_TYPE_QUESTION: "issue-type-question"; + readonly ISSUE_TYPE_QUESTION_DESCRIPTION: "issue-type-question-description"; + readonly ISSUE_TYPE_QUESTION_COLOR: "issue-type-question-color"; + readonly ISSUE_TYPE_HELP: "issue-type-help"; + readonly ISSUE_TYPE_HELP_DESCRIPTION: "issue-type-help-description"; + readonly ISSUE_TYPE_HELP_COLOR: "issue-type-help-color"; + readonly ISSUE_TYPE_TASK: "issue-type-task"; + readonly ISSUE_TYPE_TASK_DESCRIPTION: "issue-type-task-description"; + readonly ISSUE_TYPE_TASK_COLOR: "issue-type-task-color"; + readonly ISSUES_LOCALE: "issues-locale"; + readonly PULL_REQUESTS_LOCALE: "pull-requests-locale"; + readonly SIZE_XXL_THRESHOLD_LINES: "size-xxl-threshold-lines"; + readonly SIZE_XXL_THRESHOLD_FILES: "size-xxl-threshold-files"; + readonly SIZE_XXL_THRESHOLD_COMMITS: "size-xxl-threshold-commits"; + readonly SIZE_XL_THRESHOLD_LINES: "size-xl-threshold-lines"; + readonly SIZE_XL_THRESHOLD_FILES: "size-xl-threshold-files"; + readonly SIZE_XL_THRESHOLD_COMMITS: "size-xl-threshold-commits"; + readonly SIZE_L_THRESHOLD_LINES: "size-l-threshold-lines"; + readonly SIZE_L_THRESHOLD_FILES: "size-l-threshold-files"; + readonly SIZE_L_THRESHOLD_COMMITS: "size-l-threshold-commits"; + readonly SIZE_M_THRESHOLD_LINES: "size-m-threshold-lines"; + readonly SIZE_M_THRESHOLD_FILES: "size-m-threshold-files"; + readonly SIZE_M_THRESHOLD_COMMITS: "size-m-threshold-commits"; + readonly SIZE_S_THRESHOLD_LINES: "size-s-threshold-lines"; + readonly SIZE_S_THRESHOLD_FILES: "size-s-threshold-files"; + readonly SIZE_S_THRESHOLD_COMMITS: "size-s-threshold-commits"; + readonly SIZE_XS_THRESHOLD_LINES: "size-xs-threshold-lines"; + readonly SIZE_XS_THRESHOLD_FILES: "size-xs-threshold-files"; + readonly SIZE_XS_THRESHOLD_COMMITS: "size-xs-threshold-commits"; + readonly MAIN_BRANCH: "main-branch"; + readonly DEVELOPMENT_BRANCH: "development-branch"; + readonly FEATURE_TREE: "feature-tree"; + readonly BUGFIX_TREE: "bugfix-tree"; + readonly HOTFIX_TREE: "hotfix-tree"; + readonly RELEASE_TREE: "release-tree"; + readonly DOCS_TREE: "docs-tree"; + readonly CHORE_TREE: "chore-tree"; + readonly COMMIT_PREFIX_TRANSFORMS: "commit-prefix-transforms"; + readonly BRANCH_MANAGEMENT_ALWAYS: "branch-management-always"; + readonly REOPEN_ISSUE_ON_PUSH: "reopen-issue-on-push"; + readonly DESIRED_ASSIGNEES_COUNT: "desired-assignees-count"; + readonly PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: "desired-assignees-count"; + readonly PULL_REQUEST_DESIRED_REVIEWERS_COUNT: "desired-reviewers-count"; + readonly PULL_REQUEST_MERGE_TIMEOUT: "merge-timeout"; +}; diff --git a/build/github_action/src/application/contracts/product_identity.d.ts b/build/github_action/src/application/contracts/product_identity.d.ts new file mode 100644 index 00000000..9ed260e6 --- /dev/null +++ b/build/github_action/src/application/contracts/product_identity.d.ts @@ -0,0 +1 @@ +export declare const TITLE = "Copilot"; diff --git a/build/github_action/src/application/policies/bugbot_constants.d.ts b/build/github_action/src/application/policies/bugbot_constants.d.ts new file mode 100644 index 00000000..d43484a4 --- /dev/null +++ b/build/github_action/src/application/policies/bugbot_constants.d.ts @@ -0,0 +1,6 @@ +/** Hidden marker prefix used to reconcile Bugbot findings across comments. */ +export declare const BUGBOT_MARKER_PREFIX = "copilot-bugbot"; +/** Maximum number of individual Bugbot comments published for one analysis. */ +export declare const BUGBOT_MAX_COMMENTS = 20; +/** Minimum severity published by default. */ +export declare const BUGBOT_MIN_SEVERITY: 'info' | 'low' | 'medium' | 'high'; diff --git a/build/github_action/src/application/policies/setup_configuration_defaults.d.ts b/build/github_action/src/application/policies/setup_configuration_defaults.d.ts new file mode 100644 index 00000000..484a150d --- /dev/null +++ b/build/github_action/src/application/policies/setup_configuration_defaults.d.ts @@ -0,0 +1,22 @@ +import type { AgentTask } from '../../domain/agent'; +import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupResourceStoragePolicy, SetupStorageConfiguration } from '../../domain/setup'; +export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; +export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; +export declare function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration; +export declare function createDefaultSetupConfiguration(): SetupConfiguration; +export type SetupConfigurationOverrides = { + features?: Partial; + agents?: Partial>>; + repository?: Partial; + ai?: Partial; + projects?: Partial; + createInitialTag?: boolean; + manageRepositoryVariables?: boolean; + manageRepositorySecrets?: boolean; + actionInputs?: Record; + storage?: { + secrets?: Partial; + variables?: Partial; + }; +}; +export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; diff --git a/build/github_action/src/application/policies/setup_configuration_plan.d.ts b/build/github_action/src/application/policies/setup_configuration_plan.d.ts new file mode 100644 index 00000000..4314ee38 --- /dev/null +++ b/build/github_action/src/application/policies/setup_configuration_plan.d.ts @@ -0,0 +1,6 @@ +import type { SetupConfiguration, SetupCredentialRequirement, SetupPlan, SetupVariable } from '../../domain/setup'; +export declare function buildSetupPlan(configuration: SetupConfiguration): SetupPlan; +/** Builds the non-sensitive credential contract implied by the selected agents. */ +export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; +export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; +export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; diff --git a/build/github_action/src/application/policies/setup_configuration_policy.d.ts b/build/github_action/src/application/policies/setup_configuration_policy.d.ts index 67ef0424..b23fda05 100644 --- a/build/github_action/src/application/policies/setup_configuration_policy.d.ts +++ b/build/github_action/src/application/policies/setup_configuration_policy.d.ts @@ -1,41 +1,5 @@ -import type { AgentTask } from '../../domain/agent'; -import type { SetupAgentRoleConfiguration, SetupConfiguration, SetupFeatures, SetupPlan, SetupVariable, SetupCredentialRequirement, SetupResourceScope, SetupResourceStoragePolicy, SetupStorageConfiguration, SetupRemoteConfiguration, SetupResourceTarget } from '../../domain/setup'; -export declare const SETUP_AGENT_TASKS: readonly AgentTask[]; -export declare const SETUP_FEATURE_DESCRIPTIONS: Readonly>; -export declare function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration; -export declare function createDefaultSetupConfiguration(): SetupConfiguration; -export type SetupConfigurationOverrides = { - features?: Partial; - agents?: Partial>>; - repository?: Partial; - ai?: Partial; - projects?: Partial; - createInitialTag?: boolean; - manageRepositoryVariables?: boolean; - manageRepositorySecrets?: boolean; - actionInputs?: Record; - storage?: { - secrets?: Partial; - variables?: Partial; - }; -}; -export declare function mergeSetupConfiguration(base: SetupConfiguration, overrides?: SetupConfigurationOverrides): SetupConfiguration; -export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; -export declare function buildSetupPlan(configuration: SetupConfiguration): SetupPlan; -/** Builds the non-sensitive credential contract implied by the selected agents. */ -export declare function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[]; -export declare function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[]; -export declare function buildSetupActionInputs(configuration: SetupConfiguration): Record; -export declare function resolveSetupResourceScope(policy: SetupResourceStoragePolicy, name: string): SetupResourceScope; -export type SetupResourceKind = 'secret' | 'variable'; -export declare function getSetupResourceStoragePolicy(configuration: SetupConfiguration, kind: SetupResourceKind): SetupResourceStoragePolicy; -export declare function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration; -export declare function resolveSetupResourceTarget(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): SetupResourceTarget; -export declare function setupResourceExists(remote: SetupRemoteConfiguration | undefined, kind: SetupResourceKind, name: string): { - repository: boolean; - organization: boolean; - effective?: SetupResourceScope; -}; -export declare function shouldUpsertSetupResource(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): boolean; -export declare function validateSetupStorageAgainstRemote(configuration: SetupConfiguration, remote: SetupRemoteConfiguration): string[]; -export declare function usesOrganizationStorage(configuration: SetupConfiguration): boolean; +/** Public setup-policy boundary. Each concern is implemented in a focused policy module. */ +export * from './setup_configuration_defaults'; +export * from './setup_configuration_plan'; +export * from './setup_configuration_storage_policy'; +export * from './setup_configuration_validation'; diff --git a/build/github_action/src/application/policies/setup_configuration_storage_policy.d.ts b/build/github_action/src/application/policies/setup_configuration_storage_policy.d.ts new file mode 100644 index 00000000..293248aa --- /dev/null +++ b/build/github_action/src/application/policies/setup_configuration_storage_policy.d.ts @@ -0,0 +1,15 @@ +import type { SetupConfiguration, SetupRemoteConfiguration, SetupResourceScope, SetupResourceStoragePolicy, SetupResourceTarget, SetupStorageConfiguration } from '../../domain/setup'; +export type SetupResourceKind = 'secret' | 'variable'; +export declare function resolveSetupResourceScope(policy: SetupResourceStoragePolicy, name: string): SetupResourceScope; +export declare function getSetupResourceStoragePolicy(configuration: SetupConfiguration, kind: SetupResourceKind): SetupResourceStoragePolicy; +export declare function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration; +export declare function resolveSetupResourceTarget(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): SetupResourceTarget; +export declare function setupResourceExists(remote: SetupRemoteConfiguration | undefined, kind: SetupResourceKind, name: string): { + repository: boolean; + organization: boolean; + effective?: SetupResourceScope; +}; +export declare function shouldUpsertSetupResource(configuration: SetupConfiguration, kind: SetupResourceKind, name: string, remote?: SetupRemoteConfiguration): boolean; +export declare function validateSetupStorageAgainstRemote(configuration: SetupConfiguration, remote: SetupRemoteConfiguration): string[]; +export declare function usesOrganizationStorage(configuration: SetupConfiguration): boolean; +export declare function validateStorageConfiguration(storage: SetupStorageConfiguration | undefined): string[]; diff --git a/build/github_action/src/application/policies/setup_configuration_validation.d.ts b/build/github_action/src/application/policies/setup_configuration_validation.d.ts new file mode 100644 index 00000000..5d74f652 --- /dev/null +++ b/build/github_action/src/application/policies/setup_configuration_validation.d.ts @@ -0,0 +1,2 @@ +import type { SetupConfiguration } from '../../domain/setup'; +export declare function validateSetupConfiguration(configuration: SetupConfiguration): string[]; diff --git a/build/github_action/src/application/policies/workflow_queue_policy.d.ts b/build/github_action/src/application/policies/workflow_queue_policy.d.ts index f69afed1..98937d72 100644 --- a/build/github_action/src/application/policies/workflow_queue_policy.d.ts +++ b/build/github_action/src/application/policies/workflow_queue_policy.d.ts @@ -3,7 +3,7 @@ * repository mutation queue. Keep these names aligned with workflow `name` * values in `.github/workflows` and the setup templates. */ -export declare const COPILOT_WORKFLOW_NAMES: readonly ["Copilot - Issue", "Copilot - Issue Comment", "Copilot - Commit", "Copilot - Pull Request", "Copilot - Pull Request Comment", "Task - Hotfix", "Task - Release"]; +export declare const COPILOT_WORKFLOW_NAMES: readonly ["Copilot - Issue", "Copilot - Issue Comment", "Copilot - Commit", "Copilot - Pull Request", "Copilot - Pull Request Comment", "Copilot - Close Inactive Issues", "Task - Hotfix", "Task - Release"]; export interface WorkflowPollingPolicy { maximumQueueWaitMilliseconds: number; initialDelayMilliseconds: number; diff --git a/build/github_action/src/application/ports/issue_inactivity_ports.d.ts b/build/github_action/src/application/ports/issue_inactivity_ports.d.ts new file mode 100644 index 00000000..e03f76c7 --- /dev/null +++ b/build/github_action/src/application/ports/issue_inactivity_ports.d.ts @@ -0,0 +1,8 @@ +import type { IssueActivitySnapshot } from '../../domain/issue_inactivity'; +export interface IssueInactivityQueryPort { + listOpenIssuesByLabel(owner: string, repository: string, label: string, token: string): Promise; + getOpenIssue(owner: string, repository: string, issueNumber: number, token: string): Promise; +} +export interface IssueInactivityClockPort { + nowMilliseconds(): number; +} diff --git a/build/github_action/src/application/usecases/actions/close_inactive_issues_use_case.d.ts b/build/github_action/src/application/usecases/actions/close_inactive_issues_use_case.d.ts new file mode 100644 index 00000000..39e779fe --- /dev/null +++ b/build/github_action/src/application/usecases/actions/close_inactive_issues_use_case.d.ts @@ -0,0 +1,14 @@ +import type { Execution } from '../../../data/model/execution'; +import type { Result } from '../../../data/model/result'; +import { ParamUseCase } from '../base/param_usecase'; +import type { IssueClosurePort } from '../../ports/issue_lifecycle_ports'; +import type { IssueInactivityClockPort, IssueInactivityQueryPort } from '../../ports/issue_inactivity_ports'; +/** Application boundary for the scheduled inactivity-maintenance action. */ +export declare class CloseInactiveIssuesUseCase implements ParamUseCase { + private readonly issueQueryPort; + private readonly issueClosurePort; + private readonly clock; + taskId: string; + constructor(issueQueryPort: IssueInactivityQueryPort, issueClosurePort: IssueClosurePort, clock: IssueInactivityClockPort); + invoke(param: Execution): Promise; +} diff --git a/build/github_action/src/application/usecases/actions/close_inactive_issues_workflow.d.ts b/build/github_action/src/application/usecases/actions/close_inactive_issues_workflow.d.ts new file mode 100644 index 00000000..d5ed6313 --- /dev/null +++ b/build/github_action/src/application/usecases/actions/close_inactive_issues_workflow.d.ts @@ -0,0 +1,11 @@ +import type { Execution } from '../../../data/model/execution'; +import { Result } from '../../../data/model/result'; +import type { IssueClosurePort } from '../../ports/issue_lifecycle_ports'; +import type { IssueInactivityClockPort, IssueInactivityQueryPort } from '../../ports/issue_inactivity_ports'; +export interface CloseInactiveIssuesWorkflowDependencies { + readonly issueQueryPort: IssueInactivityQueryPort; + readonly issueClosurePort: IssueClosurePort; + readonly clock: IssueInactivityClockPort; +} +/** Scans waiting issues and closes only candidates that remain inactive. */ +export declare function runCloseInactiveIssuesWorkflow(param: Execution, dependencies: CloseInactiveIssuesWorkflowDependencies): Promise; diff --git a/build/github_action/src/application/usecases/actions/initial_setup_request.d.ts b/build/github_action/src/application/usecases/actions/initial_setup_request.d.ts new file mode 100644 index 00000000..615dd573 --- /dev/null +++ b/build/github_action/src/application/usecases/actions/initial_setup_request.d.ts @@ -0,0 +1,14 @@ +import type { Execution } from '../../../data/model/execution'; +import type { IssueTypes } from '../../../data/model/issue_types'; +import type { Labels } from '../../../data/model/labels'; +import type { SetupConfiguration } from '../../../domain/setup'; +import type { SetupRepositoryContext } from './setup_resource_provisioning'; +/** Narrow input assembled by the execution adapter for the setup workflow. */ +export interface InitialSetupRequest extends SetupRepositoryContext { + labels: Labels; + issueTypes: IssueTypes; + setupConfiguration?: SetupConfiguration; + workflowUpdates: readonly string[]; +} +/** Converts the legacy execution aggregate into the setup use case's explicit request. */ +export declare function createInitialSetupRequest(execution: Execution): InitialSetupRequest; diff --git a/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts b/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts index 94717181..4e863414 100644 --- a/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts +++ b/build/github_action/src/application/usecases/actions/initial_setup_workflow.d.ts @@ -1,12 +1,12 @@ -import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; import type { LatestTagQueryPort } from '../../ports/branch_tag_ports'; import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports'; import type { RepositoryTagPort, RepositoryDefaultBranchPort } from '../../ports/repository_release_ports'; import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '../../ports/issue_management_ports'; import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; -import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; -export interface InitialSetupWorkflowDependencies { +import type { SetupResourceProvisioningDependencies } from './setup_resource_provisioning'; +import type { InitialSetupRequest } from './initial_setup_request'; +export interface InitialSetupWorkflowDependencies extends SetupResourceProvisioningDependencies { authenticatedUserPort: AuthenticatedUserPort; initialLabelProvisioningPort: InitialLabelProvisioningPort; issueTypeProvisioningPort: IssueTypeProvisioningPort; @@ -14,9 +14,6 @@ export interface InitialSetupWorkflowDependencies { repositoryDefaultBranchPort: RepositoryDefaultBranchPort; repositoryTagPort: RepositoryTagPort; setupWorkspacePort: SetupWorkspacePort; - setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; - setupRepositorySecretsPort?: SetupRepositorySecretsPort; - setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; } /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ -export declare function runInitialSetupWorkflow(param: Execution, dependencies: InitialSetupWorkflowDependencies): Promise; +export declare function runInitialSetupWorkflow(request: InitialSetupRequest, dependencies: InitialSetupWorkflowDependencies): Promise; diff --git a/build/github_action/src/application/usecases/actions/setup_resource_provisioning.d.ts b/build/github_action/src/application/usecases/actions/setup_resource_provisioning.d.ts new file mode 100644 index 00000000..b484292b --- /dev/null +++ b/build/github_action/src/application/usecases/actions/setup_resource_provisioning.d.ts @@ -0,0 +1,33 @@ +import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration, SetupResourceTarget } from '../../../domain/setup'; +import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, SetupRepositoryVariablesPort } from '../../ports/setup_wizard_ports'; +export interface SetupResourceProvisioningDependencies { + setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; + setupRepositorySecretsPort?: SetupRepositorySecretsPort; + setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; +} +export interface SetupRepositoryContext { + owner: string; + repo: string; + token: string; + setupCredentials?: SetupCredentialCollection; + setupRemoteConfiguration?: SetupRemoteConfiguration; +} +export type SetupResource = { + name: string; + value: string; +}; +export type SetupResourceGroup = { + target: SetupResourceTarget; + resources: SetupResource[]; +}; +export declare function ensureRepositoryVariables(context: SetupRepositoryContext, dependencies: SetupResourceProvisioningDependencies, setupConfiguration?: SetupConfiguration, remoteConfiguration?: SetupRemoteConfiguration): Promise<{ + step?: string; + errors: string[]; +}>; +export declare function ensureRepositorySecrets(context: SetupRepositoryContext, dependencies: SetupResourceProvisioningDependencies, setupConfiguration?: SetupConfiguration, remoteConfiguration?: SetupRemoteConfiguration): Promise<{ + step?: string; + errors: string[]; +}>; +export declare function resolveRemoteConfiguration(context: SetupRepositoryContext, dependencies: SetupResourceProvisioningDependencies, setupConfiguration: SetupConfiguration | undefined, errors: string[]): Promise; +/** Groups resources by their resolved storage target so each provider call is scoped explicitly. */ +export declare function groupSetupResources(resources: readonly SetupResource[], kind: 'secret' | 'variable', configuration: SetupConfiguration, remoteConfiguration?: SetupRemoteConfiguration): SetupResourceGroup[]; diff --git a/build/github_action/src/application/usecases/single_action_use_case.d.ts b/build/github_action/src/application/usecases/single_action_use_case.d.ts index 64806c8a..a2587dee 100644 --- a/build/github_action/src/application/usecases/single_action_use_case.d.ts +++ b/build/github_action/src/application/usecases/single_action_use_case.d.ts @@ -11,7 +11,8 @@ export declare class SingleActionUseCase implements ParamUseCase, publishGithubActionUseCase: ParamUseCase, createReleaseUseCase: ParamUseCase, createTagUseCase: ParamUseCase, thinkUseCase: ParamUseCase, initialSetupUseCase: ParamUseCase, checkProgressUseCase: ParamUseCase, detectPotentialProblemsUseCase: ParamUseCase, recommendStepsUseCase: ParamUseCase); + constructor(deployedActionUseCase: ParamUseCase, publishGithubActionUseCase: ParamUseCase, createReleaseUseCase: ParamUseCase, createTagUseCase: ParamUseCase, thinkUseCase: ParamUseCase, initialSetupUseCase: ParamUseCase, checkProgressUseCase: ParamUseCase, detectPotentialProblemsUseCase: ParamUseCase, recommendStepsUseCase: ParamUseCase, closeInactiveIssuesUseCase?: ParamUseCase | undefined); invoke(param: Execution): Promise; } diff --git a/build/github_action/src/application/usecases/single_action_workflow.d.ts b/build/github_action/src/application/usecases/single_action_workflow.d.ts index dceb16ae..46784b2d 100644 --- a/build/github_action/src/application/usecases/single_action_workflow.d.ts +++ b/build/github_action/src/application/usecases/single_action_workflow.d.ts @@ -11,5 +11,6 @@ export interface SingleActionWorkflowPorts { checkProgressUseCase: ParamUseCase; detectPotentialProblemsUseCase: ParamUseCase; recommendStepsUseCase: ParamUseCase; + closeInactiveIssuesUseCase?: ParamUseCase; } export declare function runSingleActionWorkflow(param: Execution, taskId: string, ports: SingleActionWorkflowPorts): Promise; diff --git a/build/github_action/src/cli/cli_errors.d.ts b/build/github_action/src/cli/cli_errors.d.ts new file mode 100644 index 00000000..66505353 --- /dev/null +++ b/build/github_action/src/cli/cli_errors.d.ts @@ -0,0 +1,3 @@ +export declare const ERRORS: { + readonly GIT_REPOSITORY_NOT_FOUND: "❌ Git repository not found"; +}; diff --git a/build/github_action/src/cli/setup_prompt_rendering.d.ts b/build/github_action/src/cli/setup_prompt_rendering.d.ts new file mode 100644 index 00000000..2a683597 --- /dev/null +++ b/build/github_action/src/cli/setup_prompt_rendering.d.ts @@ -0,0 +1,7 @@ +import type { DoctorCheckStatus, SetupCredentialCheck, SetupCredentialRequirement, SetupRemoteConfiguration, SetupVariable } from '../domain/setup'; +export declare function statusIcon(status: SetupCredentialCheck['status']): string; +export declare function doctorIcon(status: DoctorCheckStatus): string; +export declare function formatTask(task: string): string; +export declare function color(value: string, code: number): string; +export declare function renderBox(content: string, title: string, borderCode?: number): string; +export declare function renderRemoteConfiguration(remote: SetupRemoteConfiguration, variables: readonly SetupVariable[], requirements: readonly SetupCredentialRequirement[]): string; diff --git a/build/github_action/src/data/model/action_types.d.ts b/build/github_action/src/data/model/action_types.d.ts index 9fa3a23c..4b7560c5 100644 --- a/build/github_action/src/data/model/action_types.d.ts +++ b/build/github_action/src/data/model/action_types.d.ts @@ -9,4 +9,5 @@ export declare const ACTIONS: { readonly CHECK_PROGRESS: "check_progress_action"; readonly DETECT_POTENTIAL_PROBLEMS: "detect_potential_problems_action"; readonly RECOMMEND_STEPS: "recommend_steps_action"; + readonly CLOSE_INACTIVE_ISSUES: "close_inactive_issues_action"; }; diff --git a/build/github_action/src/data/model/execution.d.ts b/build/github_action/src/data/model/execution.d.ts index 062430d2..ab02613a 100644 --- a/build/github_action/src/data/model/execution.d.ts +++ b/build/github_action/src/data/model/execution.d.ts @@ -50,6 +50,7 @@ export declare class Execution { previousConfiguration: Config | undefined; currentConfiguration: Config; tokenUser: string | undefined; + inactivityThresholdHours: number; inputs: ExecutionInputs | undefined; get eventName(): string; get actor(): string; diff --git a/build/github_action/src/data/model/execution_components.d.ts b/build/github_action/src/data/model/execution_components.d.ts index 762767a5..dd57bcfc 100644 --- a/build/github_action/src/data/model/execution_components.d.ts +++ b/build/github_action/src/data/model/execution_components.d.ts @@ -38,5 +38,6 @@ export interface ExecutionComponents { projects: Projects; tokenUser?: string; welcome?: Welcome; + inactivityThresholdHours?: number; inputs?: ExecutionInputs; } diff --git a/build/github_action/src/data/model/single_action.d.ts b/build/github_action/src/data/model/single_action.d.ts index 5ec86825..435e142c 100644 --- a/build/github_action/src/data/model/single_action.d.ts +++ b/build/github_action/src/data/model/single_action.d.ts @@ -28,6 +28,7 @@ export declare class SingleAction { get isCheckProgressAction(): boolean; get isDetectPotentialProblemsAction(): boolean; get isRecommendStepsAction(): boolean; + get isCloseInactiveIssuesAction(): boolean; get enabledSingleAction(): boolean; get validSingleAction(): boolean; get isSingleActionWithoutIssue(): boolean; diff --git a/build/github_action/src/data/repository/ai/agent_constants.d.ts b/build/github_action/src/data/repository/ai/agent_constants.d.ts new file mode 100644 index 00000000..53d3fd52 --- /dev/null +++ b/build/github_action/src/data/repository/ai/agent_constants.d.ts @@ -0,0 +1,2 @@ +/** Maximum time allowed for one external agent CLI request. */ +export declare const AGENT_REQUEST_TIMEOUT_MS = 900000; diff --git a/build/github_action/src/data/repository/issue/issue_inactivity_repository.d.ts b/build/github_action/src/data/repository/issue/issue_inactivity_repository.d.ts new file mode 100644 index 00000000..66057286 --- /dev/null +++ b/build/github_action/src/data/repository/issue/issue_inactivity_repository.d.ts @@ -0,0 +1,11 @@ +import type { IssueInactivityQueryPort } from '../../../application/ports/issue_inactivity_ports'; +import type { IssueActivitySnapshot } from '../../../domain/issue_inactivity'; +import type { GithubClientPort } from '../../../infrastructure/github/ports/github_client_provider_port'; +import type { GithubIssueInactivityClient } from '../../../infrastructure/github/ports/github_issue_provider_ports'; +/** Reads the provider's issue activity timestamp and waiting-state labels. */ +export declare class IssueInactivityRepository implements IssueInactivityQueryPort { + private readonly githubClient; + constructor(githubClient: GithubClientPort); + listOpenIssuesByLabel: (owner: string, repository: string, label: string, token: string) => Promise; + getOpenIssue: (owner: string, repository: string, issueNumber: number, token: string) => Promise; +} diff --git a/build/github_action/src/data/repository/workflow/workflow_status.d.ts b/build/github_action/src/data/repository/workflow/workflow_status.d.ts new file mode 100644 index 00000000..7643a3dc --- /dev/null +++ b/build/github_action/src/data/repository/workflow/workflow_status.d.ts @@ -0,0 +1,13 @@ +export declare const WORKFLOW_STATUS: { + readonly IN_PROGRESS: "in_progress"; + readonly QUEUED: "queued"; + readonly REQUESTED: "requested"; + readonly WAITING: "waiting"; + readonly PENDING: "pending"; + readonly COMPLETED: "completed"; + readonly FAILED: "failed"; + readonly CANCELLED: "cancelled"; + readonly SKIPPED: "skipped"; + readonly TIMED_OUT: "timed_out"; +}; +export declare const WORKFLOW_ACTIVE_STATUSES: readonly string[]; diff --git a/build/github_action/src/domain/issue_inactivity.d.ts b/build/github_action/src/domain/issue_inactivity.d.ts new file mode 100644 index 00000000..0c985716 --- /dev/null +++ b/build/github_action/src/domain/issue_inactivity.d.ts @@ -0,0 +1,30 @@ +/** Default inactivity window used by the scheduled issue-maintenance action. */ +export declare const DEFAULT_INACTIVITY_THRESHOLD_HOURS = 168; +/** Maximum supported window (one year) for a finite, operationally useful value. */ +export declare const MAX_INACTIVITY_THRESHOLD_HOURS = 8760; +export interface IssueActivitySnapshot { + readonly number: number; + readonly updatedAt?: string; + readonly isPullRequest: boolean; + readonly labels: readonly string[]; +} +export type IssueInactivityDecision = { + readonly kind: 'close'; + readonly inactiveForMilliseconds: number; +} | { + readonly kind: 'skip'; + readonly reason: 'pull-request' | 'not-waiting' | 'agent-processing' | 'missing-activity-timestamp' | 'future-activity' | 'recent-activity' | 'invalid-threshold'; +}; +export interface IssueInactivityEvaluationInput { + readonly issue: IssueActivitySnapshot; + readonly waitingLabels: readonly string[]; + readonly agentActivityLabel: string; + readonly thresholdHours: number; + readonly nowMilliseconds: number; +} +/** + * Decides whether an issue can be closed without depending on GitHub or time + * APIs. GitHub's `updated_at` is treated as the last activity observed by the + * provider; this includes comments and issue metadata changes. + */ +export declare function evaluateIssueInactivity(input: IssueInactivityEvaluationInput): IssueInactivityDecision; diff --git a/build/github_action/src/domain/setup.d.ts b/build/github_action/src/domain/setup.d.ts index 12b58f46..04095388 100644 --- a/build/github_action/src/domain/setup.d.ts +++ b/build/github_action/src/domain/setup.d.ts @@ -1,6 +1,6 @@ import type { AgentProvider, AgentTask } from './agent'; import type { PullRequestDescriptionMode } from './pull_request_description'; -export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'issueTemplates' | 'pullRequestTemplate'; +export type SetupFeature = 'issues' | 'pullRequests' | 'commits' | 'issueComments' | 'pullRequestComments' | 'release' | 'hotfix' | 'agentProvisioning' | 'credentialHealth' | 'inactiveIssueClosure' | 'issueTemplates' | 'pullRequestTemplate'; export interface SetupFeatures { [feature: string]: boolean; } @@ -25,6 +25,7 @@ export interface SetupRepositoryConfiguration { desiredAssigneesCount: number; desiredReviewersCount: number; mergeTimeout: number; + inactivityThresholdHours: number; issueLocale: string; pullRequestLocale: string; commitPrefixTransforms: string; diff --git a/build/github_action/src/infrastructure/composition/github_issue_client_factory.d.ts b/build/github_action/src/infrastructure/composition/github_issue_client_factory.d.ts index 8b2e3f91..587f1901 100644 --- a/build/github_action/src/infrastructure/composition/github_issue_client_factory.d.ts +++ b/build/github_action/src/infrastructure/composition/github_issue_client_factory.d.ts @@ -1,8 +1,9 @@ -import { OctokitIssueAssignmentClientAdapter, OctokitIssueContentClientAdapter, OctokitIssueLabelProvisioningClientAdapter, OctokitIssueLabelsClientAdapter, OctokitIssueLifecycleClientAdapter, OctokitIssueMetadataClientAdapter, OctokitIssueTitleClientAdapter } from "../github/octokit_issue_adapters"; +import { OctokitIssueAssignmentClientAdapter, OctokitIssueContentClientAdapter, OctokitIssueInactivityClientAdapter, OctokitIssueLabelProvisioningClientAdapter, OctokitIssueLabelsClientAdapter, OctokitIssueLifecycleClientAdapter, OctokitIssueMetadataClientAdapter, OctokitIssueTitleClientAdapter } from "../github/octokit_issue_adapters"; export declare const createIssueAssignmentClient: () => OctokitIssueAssignmentClientAdapter; export declare const createIssueContentClient: () => OctokitIssueContentClientAdapter; export declare const createIssueLabelProvisioningClient: () => OctokitIssueLabelProvisioningClientAdapter; export declare const createIssueLabelsClient: () => OctokitIssueLabelsClientAdapter; export declare const createIssueLifecycleClient: () => OctokitIssueLifecycleClientAdapter; +export declare const createIssueInactivityClient: () => OctokitIssueInactivityClientAdapter; export declare const createIssueMetadataClient: () => OctokitIssueMetadataClientAdapter; export declare const createIssueTitleClient: () => OctokitIssueTitleClientAdapter; diff --git a/build/github_action/src/infrastructure/composition/issue_inactivity_composition_root.d.ts b/build/github_action/src/infrastructure/composition/issue_inactivity_composition_root.d.ts new file mode 100644 index 00000000..8f1c0bfb --- /dev/null +++ b/build/github_action/src/infrastructure/composition/issue_inactivity_composition_root.d.ts @@ -0,0 +1,2 @@ +import { CloseInactiveIssuesUseCase } from '../../application/usecases/actions/close_inactive_issues_use_case'; +export declare function createCloseInactiveIssuesUseCase(): CloseInactiveIssuesUseCase; diff --git a/build/github_action/src/infrastructure/github/octokit_issue_adapters.d.ts b/build/github_action/src/infrastructure/github/octokit_issue_adapters.d.ts index 511b9fbd..7ad95b6b 100644 --- a/build/github_action/src/infrastructure/github/octokit_issue_adapters.d.ts +++ b/build/github_action/src/infrastructure/github/octokit_issue_adapters.d.ts @@ -1,5 +1,5 @@ import type { GithubClientPort } from "./ports/github_client_provider_port"; -import type { GithubIssueAssignmentClient, GithubIssueContentClient, GithubIssueLabelsClient, GithubIssueLifecycleClient, GithubIssueMetadataClient, GithubIssueTitleClient } from "./ports/github_issue_provider_ports"; +import type { GithubIssueAssignmentClient, GithubIssueContentClient, GithubIssueInactivityClient, GithubIssueLabelsClient, GithubIssueLifecycleClient, GithubIssueMetadataClient, GithubIssueTitleClient } from "./ports/github_issue_provider_ports"; import type { GithubIssueLabelProvisioningClient } from "./ports/github_issue_label_provisioning_protocol"; export declare class OctokitIssueAssignmentClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueAssignmentClient; @@ -16,6 +16,9 @@ export declare class OctokitIssueLabelsClientAdapter implements GithubClientPort export declare class OctokitIssueLifecycleClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueLifecycleClient; } +export declare class OctokitIssueInactivityClientAdapter implements GithubClientPort { + getClient(token: string): GithubIssueInactivityClient; +} export declare class OctokitIssueMetadataClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueMetadataClient; } diff --git a/build/github_action/src/infrastructure/github/ports/github_issue_provider_ports.d.ts b/build/github_action/src/infrastructure/github/ports/github_issue_provider_ports.d.ts index 2a5929a9..12d52591 100644 --- a/build/github_action/src/infrastructure/github/ports/github_issue_provider_ports.d.ts +++ b/build/github_action/src/infrastructure/github/ports/github_issue_provider_ports.d.ts @@ -10,6 +10,34 @@ export interface GithubIssueLifecycleClient { }; }; } +export interface GithubIssueInactivityClient { + paginate: { + iterator(method: (parameters: Record) => Promise<{ + data: GithubIssueActivity[]; + }>, parameters: Record): AsyncIterable<{ + data: GithubIssueActivity[]; + }>; + }; + rest: { + issues: { + listForRepo(parameters: Record): Promise<{ + data: GithubIssueActivity[]; + }>; + get(parameters: Record): Promise<{ + data: GithubIssueActivity; + }>; + }; + }; +} +export interface GithubIssueActivity { + number: number; + updated_at?: string | null; + state?: 'open' | 'closed' | string; + pull_request?: unknown; + labels?: Array<{ + name?: string; + } | string>; +} export interface GithubIssueContentClient { paginate: { iterator(method: (parameters: Record) => Promise<{ diff --git a/build/github_action/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts b/build/github_action/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts new file mode 100644 index 00000000..b2b8e31c --- /dev/null +++ b/build/github_action/src/infrastructure/time/system_issue_inactivity_clock_adapter.d.ts @@ -0,0 +1,4 @@ +import type { IssueInactivityClockPort } from '../../application/ports/issue_inactivity_ports'; +export declare class SystemIssueInactivityClockAdapter implements IssueInactivityClockPort { + nowMilliseconds(): number; +} diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 07bd96bb..95d17a28 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -103,6 +103,7 @@ Copilot provides extensive configuration options to customize your workflow. Use - `commit-prefix-transforms`: Comma-separated list of transforms for commit prefix from branch name (e.g. "replace-slash", "kebab-case"). See README for full list. - `reopen-issue-on-push`: Reopen closed issues on new commits (default: "true") - `merge-timeout`: Timeout for merge operations in seconds (default: 600) + - `inactivity-threshold-hours`: Hours without activity before a waiting issue is closed by the scheduled cleanup (default: 168; valid range: 1–8760) ## Workflow Files @@ -118,6 +119,7 @@ Copilot provides extensive configuration options to customize your workflow. Use - `single-action-version`: Version for `create_release` or `create_tag` - `single-action-title`: Title for `create_release` - `single-action-changelog`: Changelog body for `create_release` + - `inactivity-threshold-hours`: Inactivity window for `close_inactive_issues_action` (default: `168` hours; valid range: `1`–`8760`) ## Image Configuration @@ -171,6 +173,7 @@ may be forwarded through Repository Variables instead. | `single-action-version` | empty | Version used by release and tag single actions. | | `single-action-title` | empty | Title used by `create_release`. | | `single-action-changelog` | empty | Markdown body used by `create_release`. | +| `inactivity-threshold-hours` | `168` | Hours without activity before the scheduled action closes an eligible waiting issue. | | `queue-gate-only` | `false` | Internal control-plane mode used by the release and hotfix setup workflows to admit a run before mutation work. Do not use it as a replacement for a normal action invocation. | ### Task-specific agent overrides diff --git a/docs/dependency-rules.md b/docs/dependency-rules.md index 7d8bf778..56cdcdf3 100644 --- a/docs/dependency-rules.md +++ b/docs/dependency-rules.md @@ -1,9 +1,8 @@ # Dependency Rules and Architectural Invariants -This document defines the target dependency direction, the rules enforced by -tests today, and the explicitly known transitional boundaries. A target rule -must not be described as already enforced when the current source still has a -known exception. +This document defines the dependency direction and the rules enforced by the +current source and architecture tests. A target rule must not be described as +already enforced when the current source still has a known exception. ## Target direction @@ -80,8 +79,10 @@ keeps logging behavior replaceable and prevents application code from knowing about the process/GitHub logger. Application may use only the following side-effect-free shared utilities: -`comment_watermark`, `constants`, `content_utils`, `list_utils`, -`project_context_instruction`, `task_emoji`, and `title_utils`. New reusable +`comment_watermark`, `content_utils`, `list_utils`, +`project_context_instruction`, `secret_redaction`, `task_emoji`, and +`title_utils`. Action input keys and product constants belong to their owning +application/data contracts rather than a generic utility module. New reusable application behavior belongs in an application policy or port rather than in the generic utility directory. @@ -267,10 +268,11 @@ Primary tests: ## Known review targets -- classify the non-pure files under `src/data/model/` instead of declaring the - entire directory a domain layer; -- strengthen architecture tests where a documented rule is not yet executable; -- audit release/tag adapter contracts before changing their structure. +- keep `Execution` as a compatibility aggregate while preventing new use cases + from taking it when a narrower context contract is sufficient; +- keep provider-specific release/tag contracts behind application ports; +- extend the executable boundary tests when a new layer or composition root is + introduced. ## Acceptance standard diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index a44118a5..6c541c27 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -17,6 +17,13 @@ The repository separates semantic application ports from provider-specific adapt The application layer MUST NOT import GitHub SDKs, concrete CLIs, process libraries, or provider-specific protocols. Provider adapters MUST NOT define business policy. Configuration validation belongs at the boundary before execution. Prompt construction, untrusted-content handling, and GitHub publication sanitization are separate policies so no agent capability can bypass the security boundary. +The scheduled inactivity cleanup follows the same split: the pure +`issue_inactivity` policy decides eligibility, application ports expose issue +snapshots and closure commands, the repository maps GitHub's `updated_at` and +labels, and the composition root wires the adapters. The use case revalidates +each issue immediately before mutation so the periodic scan remains best-effort +and fail-closed for changed candidates. + Setup follows the same boundary: credential collection, workflow comparison, approval, and doctor decisions live in application use cases and semantic ports. GitHub Secret encryption, workflow dispatch, provider metadata requests, and the @@ -24,6 +31,24 @@ temporary health-workflow bootstrap are infrastructure adapters. Secret values never enter setup override files, Variables, logs, or the generated workflow templates. +The setup policy is intentionally split by responsibility: + +- `setup_configuration_defaults.ts` owns defaults and override merging. +- `setup_configuration_validation.ts` owns structural and semantic validation. +- `setup_configuration_storage_policy.ts` owns repository/organization scope + resolution and effective-resource preservation. +- `setup_configuration_plan.ts` owns the reviewable provisioning plan. +- `setup_resource_provisioning.ts` owns grouping and port calls for Variables + and Secrets. + +The legacy `Execution` aggregate is adapted once at +`initial_setup_request.ts`; the setup workflow consumes only the repository, +credential, label, issue-type, and setup-configuration facts it needs. +Action input keys are defined in `src/application/contracts/input_keys.ts`, +provider-independent action types remain in `src/data/model/action_types.ts`, +and unrelated constants have explicit owners instead of a global constants +module. + Runtime aggregates are kept at the composition boundary. Extracted policies and reconciliation use cases receive narrow context contracts containing only the facts they need; they do not depend on the complete `Execution` aggregate. @@ -40,7 +65,7 @@ downgrade configuration written by a newer one. ## Workflow queue boundary The repository-wide mutation queue is an application use case backed by semantic -ports. `workflow_queue_policy.ts` owns the seven shared workflow names, the 90-minute +ports. `workflow_queue_policy.ts` owns the eight shared workflow names, the 90-minute queue budget, adaptive polling schedule, jitter bounds, and retry budgets. The use case receives a clock and random-value port so deadline and delay behavior remain deterministic in tests; concrete system clock/random and timer adapters are wired in @@ -59,7 +84,7 @@ the optional workflow-scoped provider endpoint when available. Repository-only c fall back to `listWorkflowRunsForRepo` without sending `workflow_id`; an error from an invoked provider endpoint is not converted into a capability fallback. Exact counting still requires exhaustive pagination: the current provider contract cannot express the -seven workflow names, five active statuses, and strict lower-ID predicate as one safe +eight workflow names, five active statuses, and strict lower-ID predicate as one safe server-side request. `per_page: 100` and one traversal minimize fan-out, but deep-history API pressure remains a bounded-retry residual risk rather than a correctness shortcut. diff --git a/docs/development/testing.mdx b/docs/development/testing.mdx index b0d178ed..529aa3b7 100644 --- a/docs/development/testing.mdx +++ b/docs/development/testing.mdx @@ -24,7 +24,7 @@ Workflow queue tests inject a clock, random source, and scheduler/delay port. Th cover one repository traversal with mixed statuses and later-page matches, strict lower-ID/name filtering, malformed responses, fail-closed provider errors, 429 and rate-limited 403 headers, bounded fallback backoff/jitter, and the absolute deadline. -The workflow contract test keeps the seven workflow names synchronized with the +The workflow contract test keeps the eight workflow names synchronized with the validator and verifies the `90m queue / 120m job` budget, required runners, action inputs, missing/short timeouts, and forbidden mutation-workflow concurrency. diff --git a/docs/features.mdx b/docs/features.mdx index 1812c612..9ebdf2da 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -72,6 +72,17 @@ When the workflow runs on `push` (e.g. to any branch): --- +### 4. Scheduled inactivity cleanup (`on: schedule`) + +The optional `copilot_close_inactive_issues.yml` workflow runs every six hours +and invokes `close_inactive_issues_action`. It closes only open issues with +`state:awaiting-maintainer` or `state:awaiting-issue-author` whose GitHub +`updated_at` is older than `inactivity-threshold-hours` (168 hours by default). +Pull requests, issues marked `state:ai-processing`, and candidates that become +active while the scan is running are skipped. Setup keeps this feature disabled +by default because it changes issue state; enable `inactiveIssueClosure` after +reviewing the threshold and the workflow PAT permissions. + ## Single actions When you set `single-action` (and, when required, `single-action-issue`, `single-action-version`, `single-action-title`, `single-action-changelog`), the action runs **only** that action and skips the normal issue/PR/push pipelines. @@ -87,8 +98,9 @@ When you set `single-action` (and, when required, `single-action-issue`, `single | **`create_tag`** | `single-action-version` | Creates a Git tag with prefix `v` (e.g. `v1.2.0`) for the given version from the release branch. | | **`publish_github_action`** | `single-action-version` | Publishes or updates the GitHub Action: creates/updates the major version tag (for example, `v3` from a `v3.x.y` release). Requires `create_tag` to have been run first. | | **`deployed_action`** | `single-action-issue` | Marks the issue as deployed; updates labels and project state (e.g. "deployed"). | +| **`close_inactive_issues_action`** | `inactivity-threshold-hours` (optional) | Scans waiting issues and closes those inactive for the configured threshold; no issue number is required. | -Single actions that **throw an error** if the last step fails: `publish_github_action`, `create_release`, `deployed_action`, `create_tag`. This lets the workflow fail the job when the action does not succeed. +Single actions that **throw an error** if the last step fails: `publish_github_action`, `create_release`, `deployed_action`, `create_tag`, `close_inactive_issues_action`. This lets the workflow fail the job when the action does not succeed. --- @@ -146,14 +158,14 @@ an execution or publish results. ### How it works 1. For a GitHub Action run with a PAT, the action first compares the event actor with the authenticated PAT user. A normal run from the same account completes successfully before project composition, agent provisioning, setup, or queue polling. A valid explicit single action continues through the normal lifecycle. -2. An admitted run resolves the current workflow file from `GITHUB_WORKFLOW_REF` and performs one paginated repository workflow-runs traversal per poll with `per_page: 100`, then locally filters the active statuses (`in_progress`, `queued`, `requested`, `waiting`, and `pending`), the seven known Copilot/Task mutation workflow names, and runs with a **lower run ID** (i.e. started earlier). For compatibility queries that provide a workflow identifier without names, it uses the workflow-scoped endpoint when the provider exposes it; otherwise it uses the repository endpoint without `workflow_id`. +2. An admitted run resolves the current workflow file from `GITHUB_WORKFLOW_REF` and performs one paginated repository workflow-runs traversal per poll with `per_page: 100`, then locally filters the active statuses (`in_progress`, `queued`, `requested`, `waiting`, and `pending`), the eight known Copilot/Task mutation workflow names, and runs with a **lower run ID** (i.e. started earlier). For compatibility queries that provide a workflow identifier without names, it uses the workflow-scoped endpoint when the provider exposes it; otherwise it uses the repository endpoint without `workflow_id`. 3. Provider failures fail closed. Transient 408/5xx/network errors use bounded exponential retry; HTTP 429 and rate-limited 403 responses honor `Retry-After` or `x-ratelimit-reset`, then use a slower bounded fallback. Diagnostics contain only the retry reason, attempt, delay, and safe reset timestamp metadata. 4. If any such run exists, the action polls immediately and then uses adaptive 5s, 10s, 20s, 40s, and 60s maximum delays with bounded ±20% jitter. The absolute queue wait is limited to 90 minutes. 5. When no earlier active run in the mutation queue remains, the action continues. A provider failure or queue deadline never becomes an empty result, so setup and mutation work cannot proceed with an unknown queue state. The queue deliberately traverses every provider page because exact counting must detect matching runs on later pages. GitHub's current adapter contract cannot safely combine -all seven workflow names, all five active statuses, and the strict lower-ID predicate +all eight workflow names, all five active statuses, and the strict lower-ID predicate in one server-side filter. A full traversal with `per_page: 100` and one sequential request path reduces fan-out while preserving correctness; deep-history pagination therefore remains an explicit API-pressure risk, not an early-stop optimization. diff --git a/docs/graphify-development.md b/docs/graphify-development.md index 1d233a3d..2b1b088d 100644 --- a/docs/graphify-development.md +++ b/docs/graphify-development.md @@ -83,20 +83,12 @@ A RepoWise hotspot is not a refactoring instruction. Use Graphify and source search to identify the real callers and ownership first, define a semantic boundary only when one exists, and add contract tests for intentional changes. -## Last recorded checkpoint +## Current graph state -At the last published Phase D checkpoint -`af32863317977e42ec59b712fc1f371b5f231cad`, refreshed with the command above: - -```text -3271 nodes -8424 edges -217 communities -``` - -These numbers are historical navigation evidence, not a live quality score. -Regenerate the reports locally and use `git rev-parse HEAD` for the current -revision before making architecture decisions. +Graphify output is local, generated, and intentionally not a versioned source +of truth. Regenerate it with `graphify update .` and use `git rev-parse HEAD` +for the revision being audited before making architecture decisions. Do not +copy node or edge counts from an older checkpoint into current documentation. The generated graph is currently marked `directed: false`. It cannot prove the absence of directed dependency cycles. Use source imports and executable diff --git a/docs/how-to-use.mdx b/docs/how-to-use.mdx index 56a0bcdc..a382a749 100644 --- a/docs/how-to-use.mdx +++ b/docs/how-to-use.mdx @@ -287,6 +287,7 @@ All `.yml` / `.yaml` files here are copied to `.github/workflows/`. Default file | `hotfix_workflow.yml` | **Manual** (`workflow_dispatch`): hotfix flow. Filename must match the action input `hotfix-workflow` (default: `hotfix_workflow.yml`). | | `agent-cli-provisioning.yml` | **Manual** (`workflow_dispatch`): verifies that the selected agent runtimes are available, pinned, and usable on the runner. | | `copilot_credential_health.yml` | **Manual** (`workflow_dispatch`): validates selected remote credentials without exposing their values; used by `copilot doctor`. | +| `copilot_close_inactive_issues.yml` | **Scheduled every 6 hours** (also `workflow_dispatch`): closes open issues waiting for a maintainer or issue author after the configured inactivity threshold. Opt-in. | Each Copilot workflow step passes at least `token` and the configured agent CLI inputs via `vars.*`. The **release** and **hotfix** workflows are **dispatched by the action** when an issue has the deploy label and the corresponding release/hotfix context (branch, version, etc.); they are not triggered by issue events directly. @@ -305,6 +306,12 @@ project variables, and the AI/Bugbot variables. The workflow templates read configuration and must be reviewed and added deliberately; setup does not invent them from a model selection. +When enabled, inactivity cleanup also creates `INACTIVITY_THRESHOLD_HOURS` (168 +by default). The scheduled workflow closes only issues marked +`state:awaiting-maintainer` or `state:awaiting-issue-author`; it rechecks the +issue immediately before mutating it and skips candidates that became active or +entered `state:ai-processing`. + ### `setup/ISSUE_TEMPLATE/` All files here are copied to `.github/ISSUE_TEMPLATE/`. @@ -333,6 +340,7 @@ Copied to `.github/pull_request_template.md`. Used as the default body for new P After the tutorial and file customization, you can: - Set **repository or organization variables** for the agent CLI contract (`AGENT_PROVIDER`, `AGENT_MODEL_PROVIDER`, `AGENT_MODEL`, `AGENT_EFFORT`, `AGENT_ALLOWED_MODEL_PROVIDERS`, and `AGENT_ALLOWED_MODELS`) and, when needed, independent task overrides for `FINDINGS_*`, `REVIEWER_*`, `PLANNER_*`, `FIXER_*`, `TESTER_*`, and `RELEASE_*`. The supplied Copilot workflow templates forward these values to the action. Keep `CURSOR_API_KEY` available only when one of the configured task providers is Cursor. +- Enable the `inactiveIssueClosure` setup feature when you want the scheduled cleanup, and adjust `INACTIVITY_THRESHOLD_HOURS` (1–8760 hours) to match your retention policy. This feature is disabled by default because it closes GitHub issues. - Adjust **project column names** and **branch names** via action inputs so the action moves issues/PRs to the right columns and uses your branch naming. - Customize **issue templates** (copy, add fields, change labels) while keeping label and workflow names consistent as above. - Add or modify **release/hotfix** workflow steps (e.g. build, deploy) while keeping the workflow **filenames** and the action inputs `release-workflow` and `hotfix-workflow` in sync. diff --git a/docs/issues/configuration.mdx b/docs/issues/configuration.mdx index 3b35f1ea..09f10705 100644 --- a/docs/issues/configuration.mdx +++ b/docs/issues/configuration.mdx @@ -14,6 +14,7 @@ The following parameters can be configured in the workflow: #### Action Control - `single-action`: Launch single actions - `single-action-issue`: Issue target for executing single action +- `inactivity-threshold-hours`: Hours without activity before `close_inactive_issues_action` closes a waiting issue (default: `168`, valid range: `1`–`8760`) #### Branch Management - `branch-management-launcher-label`: Label to trigger branch management actions (default: "branched") @@ -150,5 +151,5 @@ SDK and HTTP server transports are not part of the current public contract. CLI - `release-workflow`: Release workflow for running release deploys (default: "release_workflow.yml") - `hotfix-workflow`: Hotfix workflow for running hotfix deploys (default: "hotfix_workflow.yml") - `merge-timeout`: Timeout for the merge workflow in seconds (default: "600") +- `inactivity-threshold-hours`: Inactivity window for the scheduled waiting-issue cleanup (default: "168" hours) - `commit-prefix-transforms`: Transforms for commit prefix from branch name (e.g. "replace-slash", "kebab-case") - diff --git a/docs/issues/index.mdx b/docs/issues/index.mdx index d1ae6f27..dc521e93 100644 --- a/docs/issues/index.mdx +++ b/docs/issues/index.mdx @@ -21,7 +21,7 @@ Copilot automates **issue tracking** so that labels, branch creation, project li Launcher label, naming conventions, and hotfix/release rules. - Commit notifications on the issue, reopen on push, and auto-close when merged. + Commit notifications on the issue, reopen on push, and auto-close when merged or inactive. All issue-related inputs: labels, branches, size, images, workflow. @@ -42,6 +42,7 @@ Copilot automates **issue tracking** so that labels, branch creation, project li | Add **`deploy`** to a release/hotfix issue | Trigger the release or hotfix workflow (e.g. deploy). | | Push commits to the issue’s branch | Post commit notifications on the issue; optionally reopen the issue if it was closed. | | Merge the branch (e.g. into develop) | Automatically close the issue when the branch is merged. | +| Leave an issue waiting for 7 days (with cleanup enabled) | Automatically close it with an explanation; reopen it with a new comment if needed. | **Bugbot** (potential problems) runs on **push** (or single action) and posts findings on the issue and on open PRs; you can ask the bot to fix findings from a comment. See [Bugbot](/bugbot) for full details. diff --git a/docs/issues/notifications-and-auto-close.mdx b/docs/issues/notifications-and-auto-close.mdx index 2d2ce2ce..3633c643 100644 --- a/docs/issues/notifications-and-auto-close.mdx +++ b/docs/issues/notifications-and-auto-close.mdx @@ -33,6 +33,19 @@ When the **branch** created for the issue (e.g. `feature/123-title`) is **merged - **How it works:** The action listens for the merge (via the push/PR pipeline and branch state). When the branch no longer exists (merged and deleted) or the merge is detected, it closes the linked issue. - **No extra input** is required for this behavior; it is part of the normal flow when the Commit and/or PR workflows run and the branch is merged. +## Auto-close by inactivity + +Waiting issues can also be closed automatically when nobody has interacted with +them for a configured period. This is an opt-in scheduled workflow, so existing +repositories do not change behavior until `copilot setup` enables the +`inactiveIssueClosure` feature or the workflow is added manually. + +- **Workflow:** `copilot_close_inactive_issues.yml`, scheduled every 6 hours and also runnable with `workflow_dispatch`. +- **Eligibility:** open issues carrying `state:awaiting-maintainer` or `state:awaiting-issue-author`. +- **Activity:** GitHub's `updated_at` timestamp is used as the last observed issue activity. Pull requests, issues with `state:ai-processing`, missing/future timestamps, and recently updated issues are skipped. +- **Default threshold:** 168 hours (7 days), configurable with `inactivity-threshold-hours` or the `INACTIVITY_THRESHOLD_HOURS` Repository Variable. Valid values are 1–8760 hours. +- **Safety:** the issue is fetched again immediately before closing. If its state, waiting label, activity timestamp, or processing marker changed, it is left open. A closing comment explains the action; adding a new comment allows the issue to be reopened and re-evaluated. + ## Summary | Behavior | Controlled by | Where it runs | @@ -40,9 +53,10 @@ When the **branch** created for the issue (e.g. `feature/123-title`) is **merged | Commit notifications on issue | Commit workflow + optional images config | Push (Commit) workflow | | Reopen closed issue on push | `reopen-issue-on-push` (default: true) | Push (Commit) workflow | | Auto-close issue when branch merged | Built-in | Push / PR workflow when merge is detected | +| Auto-close waiting issue after inactivity | `inactiveIssueClosure` + `inactivity-threshold-hours` | Scheduled workflow every 6 hours | ## Next steps -- **[Workflow setup](/issues/workflow-setup)** — Issue workflow events. +- **[Workflow setup](/issues/workflow-setup)** — Issue workflow events and scheduled inactivity cleanup. - **[Configuration](/issues/configuration)** — `reopen-issue-on-push`, images on commit. - [How to use](/how-to-use) — Full setup including Commit workflow. diff --git a/docs/issues/workflow-setup.mdx b/docs/issues/workflow-setup.mdx index 5c21e801..5565a101 100644 --- a/docs/issues/workflow-setup.mdx +++ b/docs/issues/workflow-setup.mdx @@ -70,6 +70,34 @@ Add other inputs as needed: `branch-management-launcher-label`, `desired-assigne 5. **Deploy trigger:** When the `deploy` label is added to an issue that has a release or hotfix type, the action **dispatches** the workflow named in `release-workflow` or `hotfix-workflow` (e.g. `release_workflow.yml`, `hotfix_workflow.yml`). Filenames must match exactly. +## Scheduled inactivity cleanup + +To close issues that remain waiting for a maintainer or issue author, add +`setup/workflows/copilot_close_inactive_issues.yml` to the destination repository +or enable the `inactiveIssueClosure` feature in `copilot setup`. The template runs +every six hours and can also be started manually: + +```yaml +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + inactivity_threshold_hours: + description: Hours without activity before closing a waiting issue + required: false + default: '168' + type: string +``` + +The workflow invokes `single-action: close_inactive_issues_action` and needs the +write-capable `PAT` Secret. The default threshold is 168 hours (7 days); set +`INACTIVITY_THRESHOLD_HOURS` as a Repository Variable or pass +`inactivity-threshold-hours` to override it. The action uses the existing +`state:awaiting-maintainer`, `state:awaiting-issue-author`, and +`state:ai-processing` labels, which can be customized through the normal label +inputs. + ## Next steps - **[Assignees and projects](/issues/assignees-and-projects)** — Member assignment and project linking. diff --git a/docs/single-actions/available-actions.mdx b/docs/single-actions/available-actions.mdx index b95d7437..9a4d766c 100644 --- a/docs/single-actions/available-actions.mdx +++ b/docs/single-actions/available-actions.mdx @@ -27,6 +27,7 @@ These actions need **`single-action-issue`** set to the issue number. The workfl | **`create_release`** | `single-action-version`, `single-action-title`, `single-action-changelog` | Creates a **GitHub release** with the given version, title, and changelog (markdown body). | From a workflow after tests pass; use version and changelog from your build or inputs. | | **`create_tag`** | `single-action-version` | Creates a **Git tag** with prefix `v` (e.g. `v1.2.3`) for the given version from the release branch. | When you only need a tag (e.g. for versioning) without a full release. The tag is created from the `releaseBranch` stored in issue configuration. | | **`publish_github_action`** | `single-action-version` | **Publishes or updates** the GitHub Action: creates/updates the major version tag (for example, `v3` from a `v3.x.y` release) and the corresponding GitHub Release. Requires that `create_tag` has been run first to create the source tag `v{version}`. | In a CI job that builds and publishes the action, after `create_tag` and `create_release` have run. | +| **`close_inactive_issues_action`** | — (`inactivity-threshold-hours` optional) | Scans open issues waiting for a maintainer or issue author and closes those whose `updated_at` exceeds the configured inactivity window. Revalidates each candidate before closing and posts an explanation. | From the scheduled `copilot_close_inactive_issues.yml` workflow; no issue number is required. | ## Actions that fail the job on failure @@ -36,6 +37,7 @@ These single actions **throw an error** if their last step fails, so the **workf - **`create_release`** - **`deployed_action`** - **`create_tag`** +- **`close_inactive_issues_action`** Use them when you want the workflow to **fail** if the action does not succeed (e.g. release creation or tag creation fails). @@ -56,6 +58,7 @@ The **`copilot do`** CLI command (for example, `copilot do -p "..."`) uses the c | `create_release` | — | ✅ | ✅ | ✅ | | `create_tag` | — | ✅ | — | — | | `publish_github_action` | — | ✅ | — | — | +| `close_inactive_issues_action` | — | — | — | — | ## Next steps diff --git a/docs/single-actions/configuration.mdx b/docs/single-actions/configuration.mdx index de69383d..3561bf48 100644 --- a/docs/single-actions/configuration.mdx +++ b/docs/single-actions/configuration.mdx @@ -22,6 +22,11 @@ For `check_progress_action`, `detect_potential_problems_action`, `recommend_step |-------|-------------|---------| | `single-action-issue` | Issue number to run the action on | `'123'` | +`close_inactive_issues_action` does not use `single-action-issue`. It scans the +repository's open issues carrying a waiting-state label. Configure its optional +`inactivity-threshold-hours` input (default: `168`) or the +`INACTIVITY_THRESHOLD_HOURS` Repository Variable. + ## When the action needs a version (release or tag) For `create_release` and `create_tag`: diff --git a/docs/single-actions/index.mdx b/docs/single-actions/index.mdx index f64370ae..577f0007 100644 --- a/docs/single-actions/index.mdx +++ b/docs/single-actions/index.mdx @@ -5,7 +5,7 @@ description: Run one-off actions on demand: check progress, detect problems, thi # Single Actions -When you set the **`single-action`** input (and any required targets such as `single-action-issue` or `single-action-version`), Copilot runs **only** that action and skips the normal issue, pull request, and push pipelines. Use this for on-demand runs: progress check without pushing, Bugbot detection, recommend steps, think, create release or tag, mark deployed, or initial setup. +When you set the **`single-action`** input (and any required targets such as `single-action-issue` or `single-action-version`), Copilot runs **only** that action and skips the normal issue, pull request, and push pipelines. Use this for on-demand runs: progress check without pushing, Bugbot detection, recommend steps, think, create release or tag, mark deployed, initial setup, or scheduled inactivity cleanup. @@ -33,9 +33,10 @@ When you set the **`single-action`** input (and any required targets such as `si | **Think** | Deep code analysis or questions; no issue required. Use from workflow or CLI with `-q ""`. | | **Release / Tag** | Create a GitHub release or tag with `single-action-version` (and for release: title, changelog). | | **Deployed / Setup** | Mark an issue as deployed, or run initial setup (labels, issue types). | +| **Inactive issue cleanup** | Close stale issues waiting for a maintainer or issue author; no issue number required. | -**Actions that fail the job** if the last step fails: `publish_github_action`, `create_release`, `deployed_action`, `create_tag`. The workflow will be marked as failed so you can act on it. +**Actions that fail the job** if the last step fails: `publish_github_action`, `create_release`, `deployed_action`, `create_tag`, `close_inactive_issues_action`. The workflow will be marked as failed so you can act on it. ## Next steps diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index 7d328ccb..0c178d14 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -148,7 +148,7 @@ copilot setup Run it from the target repository root. Existing setup files are not overwritten unless you approve the update prompt or pass `--update-workflows`. The interactive flow installs the selected workflows/templates, asks for agent routing and operational settings, inspects the repository's effective Actions resources, and asks separately where Secrets and Variables should live. Organization scope requires an organization-owned repository and the corresponding organization permissions on the setup PAT. Existing repository resources take precedence over organization resources; setup preserves an inherited organization resource unless you explicitly request a repository override. -The wizard covers these features: `issues`, `pullRequests`, `commits`, `issueComments`, `pullRequestComments`, `release`, `hotfix`, `agentProvisioning`, `credentialHealth`, `issueTemplates`, and `pullRequestTemplate`. The agent tasks are `planner`, `findings`, `reviewer`, `fixer`, `tester`, and `release`; each can use any of the three supported runtimes independently. Model provider, model, effort, branch strategy, locales, AI ignore patterns, project columns, Bugbot policy, provisioning mode, and initial-tag creation are also configurable. Cursor is available as an experimental runtime and is called out in the review plan with its extra credential/checksum requirements. +The wizard covers these features: `issues`, `pullRequests`, `commits`, `issueComments`, `pullRequestComments`, `release`, `hotfix`, `agentProvisioning`, `credentialHealth`, `inactiveIssueClosure`, `issueTemplates`, and `pullRequestTemplate`. The agent tasks are `planner`, `findings`, `reviewer`, `fixer`, `tester`, and `release`; each can use any of the three supported runtimes independently. Model provider, model, effort, branch strategy, locales, AI ignore patterns, project columns, Bugbot policy, provisioning mode, and initial-tag creation are also configurable. Cursor is available as an experimental runtime and is called out in the review plan with its extra credential/checksum requirements. For automation, use the same defaults without prompts: @@ -189,6 +189,7 @@ An override file can contain only non-secret values: ```yaml features: + inactiveIssueClosure: true release: false hotfix: false agents: @@ -204,6 +205,7 @@ agents: repository: mainBranch: main developmentBranch: develop + inactivityThresholdHours: 168 projects: ids: PVT_kwDOExample issueCreatedColumn: Todo diff --git a/scripts/validate-workflow-contract.cjs b/scripts/validate-workflow-contract.cjs index 70708c16..75ddde62 100644 --- a/scripts/validate-workflow-contract.cjs +++ b/scripts/validate-workflow-contract.cjs @@ -31,6 +31,7 @@ const QUEUE_WORKFLOW_MANIFEST = Object.freeze([ ['copilot_issue_comment.yml', 'Copilot - Issue Comment', 'copilot-issues'], ['copilot_pull_request.yml', 'Copilot - Pull Request', 'copilot-pull-requests'], ['copilot_pull_request_comment.yml', 'Copilot - Pull Request Comment', 'copilot-pull-requests'], + ['copilot_close_inactive_issues.yml', 'Copilot - Close Inactive Issues', 'copilot-inactive-issues'], ['hotfix_workflow.yml', 'Task - Hotfix', 'tag'], ['release_workflow.yml', 'Task - Release', 'tag'], ].map(([file, workflowName, jobId]) => ({ file, workflowName, jobId }))); diff --git a/setup/workflows/copilot_close_inactive_issues.yml b/setup/workflows/copilot_close_inactive_issues.yml new file mode 100644 index 00000000..8dbd57db --- /dev/null +++ b/setup/workflows/copilot_close_inactive_issues.yml @@ -0,0 +1,47 @@ +name: Copilot - Close Inactive Issues + +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + inactivity_threshold_hours: + description: Hours without activity before closing a waiting issue + required: false + default: '168' + type: string + +permissions: + contents: read + +jobs: + copilot-inactive-issues: + name: Copilot - Close Inactive Issues + runs-on: ubuntu-latest + timeout-minutes: 120 + permissions: + contents: read + steps: + - name: Checkout Repository + uses: actions/checkout@v5 + + - uses: vypdev/copilot@v3 + with: + single-action: close_inactive_issues_action + inactivity-threshold-hours: ${{ inputs.inactivity_threshold_hours || vars.INACTIVITY_THRESHOLD_HOURS || '168' }} + agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} + agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} + agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} + agent-effort: ${{ vars.AGENT_EFFORT }} + agent-command: ${{ vars.AGENT_COMMAND }} + findings-provider: ${{ vars.FINDINGS_PROVIDER }} + findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} + findings-model: ${{ vars.FINDINGS_MODEL }} + findings-effort: ${{ vars.FINDINGS_EFFORT }} + findings-command: ${{ vars.FINDINGS_COMMAND }} + fixer-provider: ${{ vars.FIXER_PROVIDER }} + fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} + fixer-model: ${{ vars.FIXER_MODEL }} + fixer-effort: ${{ vars.FIXER_EFFORT }} + fixer-command: ${{ vars.FIXER_COMMAND }} + token: ${{ secrets.PAT }} diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 5f790611..b78701f4 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -6,7 +6,8 @@ import { execSync } from 'child_process'; import { program } from '../cli'; import { runLocalAction } from '../actions/local_action'; -import { ACTIONS, INPUT_KEYS } from '../utils/constants'; +import { ACTIONS } from '../data/model/action_types'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; jest.mock('child_process', () => ({ execSync: jest.fn(), diff --git a/src/actions/__tests__/agent_input_builder.test.ts b/src/actions/__tests__/agent_input_builder.test.ts index 87c5355c..2f9992fa 100644 --- a/src/actions/__tests__/agent_input_builder.test.ts +++ b/src/actions/__tests__/agent_input_builder.test.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../../utils/constants'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; import { buildAgentTasksFromValues } from '../agent_input_builder'; describe('agent input builder', () => { diff --git a/src/actions/__tests__/common_action.test.ts b/src/actions/__tests__/common_action.test.ts index c52320c4..5b81a75b 100644 --- a/src/actions/__tests__/common_action.test.ts +++ b/src/actions/__tests__/common_action.test.ts @@ -219,6 +219,7 @@ describe('mainRun', () => { 'Copilot - Commit', 'Copilot - Pull Request', 'Copilot - Pull Request Comment', + 'Copilot - Close Inactive Issues', 'Task - Hotfix', 'Task - Release', ], diff --git a/src/actions/__tests__/github_action.test.ts b/src/actions/__tests__/github_action.test.ts index 23365d86..218616cd 100644 --- a/src/actions/__tests__/github_action.test.ts +++ b/src/actions/__tests__/github_action.test.ts @@ -9,7 +9,8 @@ import * as executionBuilder from '../github_action_execution'; import * as agentRuntime from '../github_action_runtime'; import * as actionCompletion from '../github_action_completion'; import { runGitHubAction } from '../github_action'; -import { ACTIONS, INPUT_KEYS } from '../../utils/constants'; +import { ACTIONS } from '../../data/model/action_types'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; jest.mock('@actions/github', () => ({ context: { diff --git a/src/actions/__tests__/image_configuration_builder.test.ts b/src/actions/__tests__/image_configuration_builder.test.ts index e202b718..01d56bdb 100644 --- a/src/actions/__tests__/image_configuration_builder.test.ts +++ b/src/actions/__tests__/image_configuration_builder.test.ts @@ -1,4 +1,5 @@ -import { DEFAULT_IMAGE_CONFIG, INPUT_KEYS } from '../../utils/constants'; +import { DEFAULT_IMAGE_CONFIG } from '../default_image_config'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; import { buildImageConfiguration } from '../image_configuration_builder'; describe('buildImageConfiguration', () => { diff --git a/src/actions/__tests__/local_action.test.ts b/src/actions/__tests__/local_action.test.ts index 2af5c15f..18a3311f 100644 --- a/src/actions/__tests__/local_action.test.ts +++ b/src/actions/__tests__/local_action.test.ts @@ -33,7 +33,7 @@ jest.mock('../../data/repository/project/project_board_query_repository', () => })); import { runLocalAction } from '../local_action'; -import { INPUT_KEYS } from '../../utils/constants'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; /** Minimal defaults so local_action can run (avoids .split on undefined). */ function minimalActionInputs(): Record { diff --git a/src/actions/agent_input_builder.ts b/src/actions/agent_input_builder.ts index 871bdeea..8e17888c 100644 --- a/src/actions/agent_input_builder.ts +++ b/src/actions/agent_input_builder.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import { buildAgentTasks } from './agent_configuration_builder'; import { DEFAULT_AGENT_MODEL, DEFAULT_AGENT_PROVIDER, DEFAULT_MODEL_PROVIDER } from '../domain/agent'; diff --git a/src/utils/constants.ts b/src/actions/default_image_config.ts similarity index 68% rename from src/utils/constants.ts rename to src/actions/default_image_config.ts index 1430d370..51232f26 100644 --- a/src/utils/constants.ts +++ b/src/actions/default_image_config.ts @@ -1,8 +1,4 @@ -export const TITLE = 'Copilot' - -/** Maximum time allowed for one external agent CLI request. */ -export const AGENT_REQUEST_TIMEOUT_MS = 900_000 - +/** Default illustration URLs used when an action does not receive custom images. */ export const DEFAULT_IMAGE_CONFIG = { issue: { automatic: [ @@ -177,277 +173,3 @@ export const DEFAULT_IMAGE_CONFIG = { ] } }; - -export const WORKFLOW_STATUS = { - IN_PROGRESS: 'in_progress', - QUEUED: 'queued', - REQUESTED: 'requested', - WAITING: 'waiting', - PENDING: 'pending', - COMPLETED: 'completed', - FAILED: 'failed', - CANCELLED: 'cancelled', - SKIPPED: 'skipped', - TIMED_OUT: 'timed_out', -}; - -export const WORKFLOW_ACTIVE_STATUSES = [ - WORKFLOW_STATUS.IN_PROGRESS, - WORKFLOW_STATUS.QUEUED, - WORKFLOW_STATUS.REQUESTED, - WORKFLOW_STATUS.WAITING, - WORKFLOW_STATUS.PENDING, -]; - -export const INPUT_KEYS = { - // Debug - DEBUG: 'debug', - - // Welcome - WELCOME_TITLE: 'welcome-title', - WELCOME_MESSAGES: 'welcome-messages', - - // Single action - SINGLE_ACTION: 'single-action', - SINGLE_ACTION_ISSUE: 'single-action-issue', - SINGLE_ACTION_VERSION: 'single-action-version', - SINGLE_ACTION_TITLE: 'single-action-title', - SINGLE_ACTION_CHANGELOG: 'single-action-changelog', - - // Tokens - TOKEN: 'token', - QUEUE_GATE_ONLY: 'queue-gate-only', - - // Agent selection - AGENT_PROVIDER: 'agent-provider', - AGENT_MODEL_PROVIDER: 'agent-model-provider', - AGENT_EFFORT: 'agent-effort', - - AGENT_MODEL: 'agent-model', - AGENT_COMMAND: 'agent-command', - FINDINGS_PROVIDER: 'findings-provider', - FINDINGS_MODEL_PROVIDER: 'findings-model-provider', - FINDINGS_EFFORT: 'findings-effort', - - FINDINGS_MODEL: 'findings-model', - FINDINGS_COMMAND: 'findings-command', - FIXER_PROVIDER: 'fixer-provider', - FIXER_MODEL_PROVIDER: 'fixer-model-provider', - FIXER_EFFORT: 'fixer-effort', - - FIXER_MODEL: 'fixer-model', - FIXER_COMMAND: 'fixer-command', - PLANNER_PROVIDER: 'planner-provider', - PLANNER_MODEL_PROVIDER: 'planner-model-provider', - PLANNER_EFFORT: 'planner-effort', - PLANNER_MODEL: 'planner-model', - PLANNER_COMMAND: 'planner-command', - REVIEWER_PROVIDER: 'reviewer-provider', - REVIEWER_MODEL_PROVIDER: 'reviewer-model-provider', - REVIEWER_EFFORT: 'reviewer-effort', - REVIEWER_MODEL: 'reviewer-model', - REVIEWER_COMMAND: 'reviewer-command', - TESTER_PROVIDER: 'tester-provider', - TESTER_MODEL_PROVIDER: 'tester-model-provider', - TESTER_EFFORT: 'tester-effort', - TESTER_MODEL: 'tester-model', - TESTER_COMMAND: 'tester-command', - RELEASE_PROVIDER: 'release-provider', - RELEASE_MODEL_PROVIDER: 'release-model-provider', - RELEASE_EFFORT: 'release-effort', - RELEASE_MODEL: 'release-model', - RELEASE_COMMAND: 'release-command', - - // AI configuration - AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', - AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', - AI_MEMBERS_ONLY: 'ai-members-only', - AI_IGNORE_FILES: 'ai-ignore-files', - AI_INCLUDE_REASONING: 'ai-include-reasoning', - BUGBOT_SEVERITY: 'bugbot-severity', - BUGBOT_COMMENT_LIMIT: 'bugbot-comment-limit', - BUGBOT_FIX_VERIFY_COMMANDS: 'bugbot-fix-verify-commands', - - // Projects - PROJECT_IDS: 'project-ids', - PROJECT_COLUMN_ISSUE_CREATED: 'project-column-issue-created', - PROJECT_COLUMN_PULL_REQUEST_CREATED: 'project-column-pull-request-created', - PROJECT_COLUMN_ISSUE_IN_PROGRESS: 'project-column-issue-in-progress', - PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: 'project-column-pull-request-in-progress', - - // Images - IMAGES_ON_ISSUE: 'images-on-issue', - IMAGES_ON_PULL_REQUEST: 'images-on-pull-request', - IMAGES_ON_COMMIT: 'images-on-commit', - IMAGES_ISSUE_AUTOMATIC: 'images-issue-automatic', - IMAGES_ISSUE_FEATURE: 'images-issue-feature', - IMAGES_ISSUE_BUGFIX: 'images-issue-bugfix', - IMAGES_ISSUE_DOCS: 'images-issue-docs', - IMAGES_ISSUE_CHORE: 'images-issue-chore', - IMAGES_ISSUE_RELEASE: 'images-issue-release', - IMAGES_ISSUE_HOTFIX: 'images-issue-hotfix', - IMAGES_PULL_REQUEST_AUTOMATIC: 'images-pull-request-automatic', - IMAGES_PULL_REQUEST_FEATURE: 'images-pull-request-feature', - IMAGES_PULL_REQUEST_BUGFIX: 'images-pull-request-bugfix', - IMAGES_PULL_REQUEST_RELEASE: 'images-pull-request-release', - IMAGES_PULL_REQUEST_HOTFIX: 'images-pull-request-hotfix', - IMAGES_PULL_REQUEST_DOCS: 'images-pull-request-docs', - IMAGES_PULL_REQUEST_CHORE: 'images-pull-request-chore', - IMAGES_COMMIT_AUTOMATIC: 'images-commit-automatic', - IMAGES_COMMIT_FEATURE: 'images-commit-feature', - IMAGES_COMMIT_BUGFIX: 'images-commit-bugfix', - IMAGES_COMMIT_RELEASE: 'images-commit-release', - IMAGES_COMMIT_HOTFIX: 'images-commit-hotfix', - IMAGES_COMMIT_DOCS: 'images-commit-docs', - IMAGES_COMMIT_CHORE: 'images-commit-chore', - - // Workflows - RELEASE_WORKFLOW: 'release-workflow', - HOTFIX_WORKFLOW: 'hotfix-workflow', - - // Emoji - EMOJI_LABELED_TITLE: 'emoji-labeled-title', - BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', - - // Labels - BRANCH_MANAGEMENT_LAUNCHER_LABEL: 'branch-management-launcher-label', - BUGFIX_LABEL: 'bugfix-label', - BUG_LABEL: 'bug-label', - HOTFIX_LABEL: 'hotfix-label', - ENHANCEMENT_LABEL: 'enhancement-label', - FEATURE_LABEL: 'feature-label', - RELEASE_LABEL: 'release-label', - QUESTION_LABEL: 'question-label', - HELP_LABEL: 'help-label', - DEPLOY_LABEL: 'deploy-label', - DEPLOYED_LABEL: 'deployed-label', - DOCS_LABEL: 'docs-label', - DOCUMENTATION_LABEL: 'documentation-label', - CHORE_LABEL: 'chore-label', - MAINTENANCE_LABEL: 'maintenance-label', - PRIORITY_HIGH_LABEL: 'priority-high-label', - PRIORITY_MEDIUM_LABEL: 'priority-medium-label', - PRIORITY_LOW_LABEL: 'priority-low-label', - PRIORITY_NONE_LABEL: 'priority-none-label', - SIZE_XXL_LABEL: 'size-xxl-label', - SIZE_XL_LABEL: 'size-xl-label', - SIZE_L_LABEL: 'size-l-label', - SIZE_M_LABEL: 'size-m-label', - SIZE_S_LABEL: 'size-s-label', - SIZE_XS_LABEL: 'size-xs-label', - - // Lifecycle label inputs - STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', - STATE_PLANNED_LABEL: 'state-planned-label', - STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', - STATE_REVIEWING_LABEL: 'state-reviewing-label', - STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', - STATE_VERIFIED_LABEL: 'state-verified-label', - STATE_READY_LABEL: 'state-ready-label', - STATE_BLOCKED_LABEL: 'state-blocked-label', - STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', - STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', - - // Issue Types - ISSUE_TYPE_BUG: 'issue-type-bug', - ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', - ISSUE_TYPE_BUG_COLOR: 'issue-type-bug-color', - - ISSUE_TYPE_HOTFIX: 'issue-type-hotfix', - ISSUE_TYPE_HOTFIX_DESCRIPTION: 'issue-type-hotfix-description', - ISSUE_TYPE_HOTFIX_COLOR: 'issue-type-hotfix-color', - - ISSUE_TYPE_FEATURE: 'issue-type-feature', - ISSUE_TYPE_FEATURE_DESCRIPTION: 'issue-type-feature-description', - ISSUE_TYPE_FEATURE_COLOR: 'issue-type-feature-color', - - ISSUE_TYPE_DOCUMENTATION: 'issue-type-documentation', - ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: 'issue-type-documentation-description', - ISSUE_TYPE_DOCUMENTATION_COLOR: 'issue-type-documentation-color', - - ISSUE_TYPE_MAINTENANCE: 'issue-type-maintenance', - ISSUE_TYPE_MAINTENANCE_DESCRIPTION: 'issue-type-maintenance-description', - ISSUE_TYPE_MAINTENANCE_COLOR: 'issue-type-maintenance-color', - - ISSUE_TYPE_RELEASE: 'issue-type-release', - ISSUE_TYPE_RELEASE_DESCRIPTION: 'issue-type-release-description', - ISSUE_TYPE_RELEASE_COLOR: 'issue-type-release-color', - - ISSUE_TYPE_QUESTION: 'issue-type-question', - ISSUE_TYPE_QUESTION_DESCRIPTION: 'issue-type-question-description', - ISSUE_TYPE_QUESTION_COLOR: 'issue-type-question-color', - - ISSUE_TYPE_HELP: 'issue-type-help', - ISSUE_TYPE_HELP_DESCRIPTION: 'issue-type-help-description', - ISSUE_TYPE_HELP_COLOR: 'issue-type-help-color', - - ISSUE_TYPE_TASK: 'issue-type-task', - ISSUE_TYPE_TASK_DESCRIPTION: 'issue-type-task-description', - ISSUE_TYPE_TASK_COLOR: 'issue-type-task-color', - - // Locale - ISSUES_LOCALE: 'issues-locale', - PULL_REQUESTS_LOCALE: 'pull-requests-locale', - - // Size Thresholds - SIZE_XXL_THRESHOLD_LINES: 'size-xxl-threshold-lines', - SIZE_XXL_THRESHOLD_FILES: 'size-xxl-threshold-files', - SIZE_XXL_THRESHOLD_COMMITS: 'size-xxl-threshold-commits', - SIZE_XL_THRESHOLD_LINES: 'size-xl-threshold-lines', - SIZE_XL_THRESHOLD_FILES: 'size-xl-threshold-files', - SIZE_XL_THRESHOLD_COMMITS: 'size-xl-threshold-commits', - SIZE_L_THRESHOLD_LINES: 'size-l-threshold-lines', - SIZE_L_THRESHOLD_FILES: 'size-l-threshold-files', - SIZE_L_THRESHOLD_COMMITS: 'size-l-threshold-commits', - SIZE_M_THRESHOLD_LINES: 'size-m-threshold-lines', - SIZE_M_THRESHOLD_FILES: 'size-m-threshold-files', - SIZE_M_THRESHOLD_COMMITS: 'size-m-threshold-commits', - SIZE_S_THRESHOLD_LINES: 'size-s-threshold-lines', - SIZE_S_THRESHOLD_FILES: 'size-s-threshold-files', - SIZE_S_THRESHOLD_COMMITS: 'size-s-threshold-commits', - SIZE_XS_THRESHOLD_LINES: 'size-xs-threshold-lines', - SIZE_XS_THRESHOLD_FILES: 'size-xs-threshold-files', - SIZE_XS_THRESHOLD_COMMITS: 'size-xs-threshold-commits', - - // Branches - MAIN_BRANCH: 'main-branch', - DEVELOPMENT_BRANCH: 'development-branch', - FEATURE_TREE: 'feature-tree', - BUGFIX_TREE: 'bugfix-tree', - HOTFIX_TREE: 'hotfix-tree', - RELEASE_TREE: 'release-tree', - DOCS_TREE: 'docs-tree', - CHORE_TREE: 'chore-tree', - - // Commit - COMMIT_PREFIX_TRANSFORMS: 'commit-prefix-transforms', - - // Issue - BRANCH_MANAGEMENT_ALWAYS: 'branch-management-always', - REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', - DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', - - // Pull Request - PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', - PULL_REQUEST_DESIRED_REVIEWERS_COUNT: 'desired-reviewers-count', - PULL_REQUEST_MERGE_TIMEOUT: 'merge-timeout', - -} as const; - -export const ERRORS = { - GIT_REPOSITORY_NOT_FOUND: '❌ Git repository not found' -} as const; - -export { ACTIONS } from '../data/model/action_types'; - -/** Hidden HTML comment prefix for bugbot findings (issue/PR comments). Format: */ -export const BUGBOT_MARKER_PREFIX = 'copilot-bugbot'; - -/** Max number of individual bugbot comments to create per issue/PR. Excess findings get one summary comment suggesting to review locally. */ -export const BUGBOT_MAX_COMMENTS = 20; - -/** Minimum severity to publish (findings below this are dropped). Order: high > medium > low > info. */ -export const BUGBOT_MIN_SEVERITY: 'info' | 'low' | 'medium' | 'high' = 'low'; - -export const PROMPTS = { -} as const; diff --git a/src/actions/github_action.ts b/src/actions/github_action.ts index 3e725aef..d65b6bc3 100644 --- a/src/actions/github_action.ts +++ b/src/actions/github_action.ts @@ -12,7 +12,7 @@ import { buildGithubActionExecution, readGithubActionSingleAction } from './gith import { buildGithubActionEventInputs } from './github_event_inputs'; import { mainRun } from './common_action'; import { waitForPreviousWorkflowRuns, WorkflowQueueFailureError, WORKFLOW_QUEUE_FAILURE_MESSAGE } from './main_run_lifecycle'; -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import { logDebugInfo, logError, logInfo } from '../utils/logger'; import { createGithubExecutionAdmissionUseCase } from '../infrastructure/composition/github_execution_admission_composition_root'; import { createSynchronizeLifecycleStateUseCase } from '../infrastructure/composition/lifecycle_state_composition_root'; diff --git a/src/actions/github_action_ai_inputs.ts b/src/actions/github_action_ai_inputs.ts index 71a8d3a8..3213fc97 100644 --- a/src/actions/github_action_ai_inputs.ts +++ b/src/actions/github_action_ai_inputs.ts @@ -1,4 +1,5 @@ -import { BUGBOT_MAX_COMMENTS, BUGBOT_MIN_SEVERITY, INPUT_KEYS } from '../utils/constants'; +import { BUGBOT_MAX_COMMENTS, BUGBOT_MIN_SEVERITY } from '../application/policies/bugbot_constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import { isEnabledInput } from './input_boolean_policy'; import { parseBoundedPositiveIntegerInput } from './input_number_policy'; import { parseDelimitedValues } from './input_values_policy'; diff --git a/src/actions/github_action_branch_inputs.ts b/src/actions/github_action_branch_inputs.ts index 65b62a9b..1f19e964 100644 --- a/src/actions/github_action_branch_inputs.ts +++ b/src/actions/github_action_branch_inputs.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import type { BranchValues } from './branches_builder'; export function readGithubActionBranchInputs(getInput: (key: string) => string): BranchValues { diff --git a/src/actions/github_action_execution.ts b/src/actions/github_action_execution.ts index 62f143de..ec33e542 100644 --- a/src/actions/github_action_execution.ts +++ b/src/actions/github_action_execution.ts @@ -4,10 +4,10 @@ import { Release } from '../data/model/release'; import { SingleAction } from '../data/model/single_action'; import type { Execution } from '../data/model/execution'; import type { ProjectDetailQueryPort } from '../application/ports/project_detail_ports'; -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import { isEnabledInput } from './input_boolean_policy'; import { getGithubActionInput } from './github_action_input'; -import { parseIntegerInput, parseNonNegativeIntegerInput } from './input_number_policy'; +import { parseBoundedPositiveIntegerInput, parseIntegerInput, parseNonNegativeIntegerInput } from './input_number_policy'; import { parseDelimitedValues } from './input_values_policy'; import { readGithubActionAiInputs } from './github_action_ai_inputs'; import { prepareGithubAgentRuntime } from './github_action_runtime'; @@ -25,6 +25,7 @@ import { buildExecution } from './execution_builder'; import { buildEmoji, buildImages, buildIssue, buildIssueTypes, buildLabels, buildLocale, buildProjects, buildPullRequest, buildTokens, buildWorkflows } from './configuration_builders'; import { loadProjectDetails } from './project_details_loader'; import type { buildGithubActionEventInputs } from './github_event_inputs'; +import { DEFAULT_INACTIVITY_THRESHOLD_HOURS, MAX_INACTIVITY_THRESHOLD_HOURS } from '../domain/issue_inactivity'; export interface GithubActionExecutionInput { readonly getInput: typeof getGithubActionInput; @@ -41,7 +42,9 @@ export async function buildGithubActionExecution( ): Promise { const { getInput, eventInputs, projectQuery, debug, singleAction, token } = input; const aiInputs = readGithubActionAiInputs(getInput); - prepareGithubAgentRuntime(aiInputs.requestedAgentTasks); + if (!singleAction.isCloseInactiveIssuesAction) { + prepareGithubAgentRuntime(aiInputs.requestedAgentTasks); + } const projects = await loadProjectDetails( projectQuery, @@ -60,6 +63,11 @@ export async function buildGithubActionExecution( return buildExecution({ debug, + inactivityThresholdHours: parseBoundedPositiveIntegerInput( + getInput(INPUT_KEYS.INACTIVITY_THRESHOLD_HOURS), + DEFAULT_INACTIVITY_THRESHOLD_HOURS, + MAX_INACTIVITY_THRESHOLD_HOURS, + ), singleAction, commitPrefixBuilder: getCommitPrefixBuilder(getInput), issue: buildIssue( diff --git a/src/actions/github_action_issue_type_inputs.ts b/src/actions/github_action_issue_type_inputs.ts index c6919be4..3e8054a3 100644 --- a/src/actions/github_action_issue_type_inputs.ts +++ b/src/actions/github_action_issue_type_inputs.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import type { IssueTypeConfigurationValues } from './configuration_builders'; function readIssueType(getInput: (key: string) => string, name: string, description: string, color: string) { diff --git a/src/actions/github_action_label_inputs.ts b/src/actions/github_action_label_inputs.ts index 12edf813..da1352a7 100644 --- a/src/actions/github_action_label_inputs.ts +++ b/src/actions/github_action_label_inputs.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import type { LabelValues } from './configuration_builders'; export function readGithubActionLabelInputs(getInput: (key: string) => string): LabelValues { diff --git a/src/actions/github_action_locale_inputs.ts b/src/actions/github_action_locale_inputs.ts index d73ce858..660620d1 100644 --- a/src/actions/github_action_locale_inputs.ts +++ b/src/actions/github_action_locale_inputs.ts @@ -1,5 +1,5 @@ import { Locale } from '../data/model/locale'; -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; export interface GithubActionLocaleInputs { readonly issue: string; diff --git a/src/actions/github_action_project_inputs.ts b/src/actions/github_action_project_inputs.ts index e93314a2..cd7f6f16 100644 --- a/src/actions/github_action_project_inputs.ts +++ b/src/actions/github_action_project_inputs.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import type { ProjectConfigurationValues } from './configuration_builders'; import type { ProjectDetail } from '../data/model/project_detail'; diff --git a/src/actions/github_action_threshold_inputs.ts b/src/actions/github_action_threshold_inputs.ts index e6cde991..d3fcfc2e 100644 --- a/src/actions/github_action_threshold_inputs.ts +++ b/src/actions/github_action_threshold_inputs.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import { parseIntegerInput } from './input_number_policy'; import type { SizeThresholdSet } from './size_threshold_builder'; diff --git a/src/actions/github_action_workflow_inputs.ts b/src/actions/github_action_workflow_inputs.ts index f2f99e40..e4eb2b6c 100644 --- a/src/actions/github_action_workflow_inputs.ts +++ b/src/actions/github_action_workflow_inputs.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../utils/constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; export interface GithubActionWorkflowInputs { readonly release: string; diff --git a/src/actions/image_configuration_builder.ts b/src/actions/image_configuration_builder.ts index 8b961a4a..cf967004 100644 --- a/src/actions/image_configuration_builder.ts +++ b/src/actions/image_configuration_builder.ts @@ -1,4 +1,5 @@ -import { DEFAULT_IMAGE_CONFIG, INPUT_KEYS } from '../utils/constants'; +import { DEFAULT_IMAGE_CONFIG } from './default_image_config'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import { isEnabledInput } from './input_boolean_policy'; import { parseDelimitedValues } from './input_values_policy'; diff --git a/src/actions/local_action_configuration_sections.ts b/src/actions/local_action_configuration_sections.ts index c699df43..04243062 100644 --- a/src/actions/local_action_configuration_sections.ts +++ b/src/actions/local_action_configuration_sections.ts @@ -1,5 +1,6 @@ import { Locale } from '../data/model/locale'; -import { BUGBOT_MAX_COMMENTS, BUGBOT_MIN_SEVERITY, INPUT_KEYS } from '../utils/constants'; +import { BUGBOT_MAX_COMMENTS, BUGBOT_MIN_SEVERITY } from '../application/policies/bugbot_constants'; +import { INPUT_KEYS } from '../application/contracts/input_keys'; import type { ProjectDetailQueryPort } from '../application/ports/project_detail_ports'; import type { ActionInputValues } from './action_input_source'; import { getActionInputsWithDefaults } from '../utils/yml_utils'; @@ -11,6 +12,7 @@ import { parseDelimitedValues } from './input_values_policy'; import { buildAgentTasksFromValues } from './agent_input_builder'; import { buildImageConfiguration } from './image_configuration_builder'; import { normalizePullRequestDescriptionMode } from '../domain/pull_request_description'; +import { DEFAULT_INACTIVITY_THRESHOLD_HOURS, MAX_INACTIVITY_THRESHOLD_HOURS } from '../domain/issue_inactivity'; export type LocalActionInputs = ReturnType; @@ -32,6 +34,11 @@ export function readLocalCoreConfiguration( singleActionVersion: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_VERSION), singleActionTitle: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_TITLE), singleActionChangelog: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_CHANGELOG), + inactivityThresholdHours: parseBoundedPositiveIntegerInput( + input(additionalParams, actionInputs, INPUT_KEYS.INACTIVITY_THRESHOLD_HOURS), + DEFAULT_INACTIVITY_THRESHOLD_HOURS, + MAX_INACTIVITY_THRESHOLD_HOURS, + ), token: input(additionalParams, actionInputs, INPUT_KEYS.TOKEN), }; } diff --git a/src/actions/local_action_execution.ts b/src/actions/local_action_execution.ts index 7154cff9..ee782740 100644 --- a/src/actions/local_action_execution.ts +++ b/src/actions/local_action_execution.ts @@ -15,6 +15,7 @@ export function buildLocalActionExecution( ) { const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, + inactivityThresholdHours, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, @@ -40,6 +41,7 @@ export function buildLocalActionExecution( } = configuration; return buildExecution({ debug, + inactivityThresholdHours, singleAction: new SingleAction( singleAction, singleActionIssue, diff --git a/src/actions/local_action_output.ts b/src/actions/local_action_output.ts index 13ae89b0..a040cabb 100644 --- a/src/actions/local_action_output.ts +++ b/src/actions/local_action_output.ts @@ -1,6 +1,6 @@ import chalk from 'chalk'; import boxen from 'boxen'; -import { TITLE } from '../utils/constants'; +import { TITLE } from '../application/contracts/product_identity'; import { logInfo } from '../utils/logger'; type LocalActionResult = { diff --git a/src/actions/main_run_lifecycle.ts b/src/actions/main_run_lifecycle.ts index f049cc73..585a6566 100644 --- a/src/actions/main_run_lifecycle.ts +++ b/src/actions/main_run_lifecycle.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import boxen from 'boxen'; import type { Execution } from '../data/model/execution'; import type { Result } from '../data/model/result'; -import { TITLE } from '../utils/constants'; +import { TITLE } from '../application/contracts/product_identity'; import { logError, logInfo } from '../utils/logger'; import { dispatchMainRunRoute } from './main_run_dispatcher'; import type { ExecutableMainRunRoute, MainRunRouteHandlers } from './main_run_route_handlers'; diff --git a/src/application/__tests__/architecture_boundaries.test.ts b/src/application/__tests__/architecture_boundaries.test.ts index b099d2ad..f8c8a9cf 100644 --- a/src/application/__tests__/architecture_boundaries.test.ts +++ b/src/application/__tests__/architecture_boundaries.test.ts @@ -96,7 +96,6 @@ describe('application architecture boundaries', () => { it('allows only side-effect-free shared utilities in application production code', () => { const allowedUtilities = new Set([ 'comment_watermark', - 'constants', 'content_utils', 'list_utils', 'project_context_instruction', diff --git a/src/application/contracts/input_keys.ts b/src/application/contracts/input_keys.ts new file mode 100644 index 00000000..621ac2f0 --- /dev/null +++ b/src/application/contracts/input_keys.ts @@ -0,0 +1,236 @@ +/** Canonical action and CLI input vocabulary shared by input mappers. */ +export const INPUT_KEYS = { + // Debug + DEBUG: 'debug', + + // Welcome + WELCOME_TITLE: 'welcome-title', + WELCOME_MESSAGES: 'welcome-messages', + + // Single action + SINGLE_ACTION: 'single-action', + SINGLE_ACTION_ISSUE: 'single-action-issue', + SINGLE_ACTION_VERSION: 'single-action-version', + SINGLE_ACTION_TITLE: 'single-action-title', + SINGLE_ACTION_CHANGELOG: 'single-action-changelog', + INACTIVITY_THRESHOLD_HOURS: 'inactivity-threshold-hours', + + // Tokens + TOKEN: 'token', + QUEUE_GATE_ONLY: 'queue-gate-only', + + // Agent selection + AGENT_PROVIDER: 'agent-provider', + AGENT_MODEL_PROVIDER: 'agent-model-provider', + AGENT_EFFORT: 'agent-effort', + + AGENT_MODEL: 'agent-model', + AGENT_COMMAND: 'agent-command', + FINDINGS_PROVIDER: 'findings-provider', + FINDINGS_MODEL_PROVIDER: 'findings-model-provider', + FINDINGS_EFFORT: 'findings-effort', + + FINDINGS_MODEL: 'findings-model', + FINDINGS_COMMAND: 'findings-command', + FIXER_PROVIDER: 'fixer-provider', + FIXER_MODEL_PROVIDER: 'fixer-model-provider', + FIXER_EFFORT: 'fixer-effort', + + FIXER_MODEL: 'fixer-model', + FIXER_COMMAND: 'fixer-command', + PLANNER_PROVIDER: 'planner-provider', + PLANNER_MODEL_PROVIDER: 'planner-model-provider', + PLANNER_EFFORT: 'planner-effort', + PLANNER_MODEL: 'planner-model', + PLANNER_COMMAND: 'planner-command', + REVIEWER_PROVIDER: 'reviewer-provider', + REVIEWER_MODEL_PROVIDER: 'reviewer-model-provider', + REVIEWER_EFFORT: 'reviewer-effort', + REVIEWER_MODEL: 'reviewer-model', + REVIEWER_COMMAND: 'reviewer-command', + TESTER_PROVIDER: 'tester-provider', + TESTER_MODEL_PROVIDER: 'tester-model-provider', + TESTER_EFFORT: 'tester-effort', + TESTER_MODEL: 'tester-model', + TESTER_COMMAND: 'tester-command', + RELEASE_PROVIDER: 'release-provider', + RELEASE_MODEL_PROVIDER: 'release-model-provider', + RELEASE_EFFORT: 'release-effort', + RELEASE_MODEL: 'release-model', + RELEASE_COMMAND: 'release-command', + + // AI configuration + AI_PULL_REQUEST_DESCRIPTION: 'ai-pull-request-description', + AI_PULL_REQUEST_DESCRIPTION_MODE: 'ai-pull-request-description-mode', + AI_MEMBERS_ONLY: 'ai-members-only', + AI_IGNORE_FILES: 'ai-ignore-files', + AI_INCLUDE_REASONING: 'ai-include-reasoning', + BUGBOT_SEVERITY: 'bugbot-severity', + BUGBOT_COMMENT_LIMIT: 'bugbot-comment-limit', + BUGBOT_FIX_VERIFY_COMMANDS: 'bugbot-fix-verify-commands', + + // Projects + PROJECT_IDS: 'project-ids', + PROJECT_COLUMN_ISSUE_CREATED: 'project-column-issue-created', + PROJECT_COLUMN_PULL_REQUEST_CREATED: 'project-column-pull-request-created', + PROJECT_COLUMN_ISSUE_IN_PROGRESS: 'project-column-issue-in-progress', + PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS: 'project-column-pull-request-in-progress', + + // Images + IMAGES_ON_ISSUE: 'images-on-issue', + IMAGES_ON_PULL_REQUEST: 'images-on-pull-request', + IMAGES_ON_COMMIT: 'images-on-commit', + IMAGES_ISSUE_AUTOMATIC: 'images-issue-automatic', + IMAGES_ISSUE_FEATURE: 'images-issue-feature', + IMAGES_ISSUE_BUGFIX: 'images-issue-bugfix', + IMAGES_ISSUE_DOCS: 'images-issue-docs', + IMAGES_ISSUE_CHORE: 'images-issue-chore', + IMAGES_ISSUE_RELEASE: 'images-issue-release', + IMAGES_ISSUE_HOTFIX: 'images-issue-hotfix', + IMAGES_PULL_REQUEST_AUTOMATIC: 'images-pull-request-automatic', + IMAGES_PULL_REQUEST_FEATURE: 'images-pull-request-feature', + IMAGES_PULL_REQUEST_BUGFIX: 'images-pull-request-bugfix', + IMAGES_PULL_REQUEST_RELEASE: 'images-pull-request-release', + IMAGES_PULL_REQUEST_HOTFIX: 'images-pull-request-hotfix', + IMAGES_PULL_REQUEST_DOCS: 'images-pull-request-docs', + IMAGES_PULL_REQUEST_CHORE: 'images-pull-request-chore', + IMAGES_COMMIT_AUTOMATIC: 'images-commit-automatic', + IMAGES_COMMIT_FEATURE: 'images-commit-feature', + IMAGES_COMMIT_BUGFIX: 'images-commit-bugfix', + IMAGES_COMMIT_RELEASE: 'images-commit-release', + IMAGES_COMMIT_HOTFIX: 'images-commit-hotfix', + IMAGES_COMMIT_DOCS: 'images-commit-docs', + IMAGES_COMMIT_CHORE: 'images-commit-chore', + + // Workflows + RELEASE_WORKFLOW: 'release-workflow', + HOTFIX_WORKFLOW: 'hotfix-workflow', + + // Emoji + EMOJI_LABELED_TITLE: 'emoji-labeled-title', + BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', + + // Labels + BRANCH_MANAGEMENT_LAUNCHER_LABEL: 'branch-management-launcher-label', + BUGFIX_LABEL: 'bugfix-label', + BUG_LABEL: 'bug-label', + HOTFIX_LABEL: 'hotfix-label', + ENHANCEMENT_LABEL: 'enhancement-label', + FEATURE_LABEL: 'feature-label', + RELEASE_LABEL: 'release-label', + QUESTION_LABEL: 'question-label', + HELP_LABEL: 'help-label', + DEPLOY_LABEL: 'deploy-label', + DEPLOYED_LABEL: 'deployed-label', + DOCS_LABEL: 'docs-label', + DOCUMENTATION_LABEL: 'documentation-label', + CHORE_LABEL: 'chore-label', + MAINTENANCE_LABEL: 'maintenance-label', + PRIORITY_HIGH_LABEL: 'priority-high-label', + PRIORITY_MEDIUM_LABEL: 'priority-medium-label', + PRIORITY_LOW_LABEL: 'priority-low-label', + PRIORITY_NONE_LABEL: 'priority-none-label', + SIZE_XXL_LABEL: 'size-xxl-label', + SIZE_XL_LABEL: 'size-xl-label', + SIZE_L_LABEL: 'size-l-label', + SIZE_M_LABEL: 'size-m-label', + SIZE_S_LABEL: 'size-s-label', + SIZE_XS_LABEL: 'size-xs-label', + + // Lifecycle label inputs + STATE_AI_PROCESSING_LABEL: 'state-ai-processing-label', + STATE_PLANNED_LABEL: 'state-planned-label', + STATE_IN_PROGRESS_LABEL: 'state-in-progress-label', + STATE_REVIEWING_LABEL: 'state-reviewing-label', + STATE_CHANGES_REQUESTED_LABEL: 'state-changes-requested-label', + STATE_VERIFIED_LABEL: 'state-verified-label', + STATE_READY_LABEL: 'state-ready-label', + STATE_BLOCKED_LABEL: 'state-blocked-label', + STATE_AWAITING_MAINTAINER_LABEL: 'state-awaiting-maintainer-label', + STATE_AWAITING_ISSUE_AUTHOR_LABEL: 'state-awaiting-issue-author-label', + + // Issue Types + ISSUE_TYPE_BUG: 'issue-type-bug', + ISSUE_TYPE_BUG_DESCRIPTION: 'issue-type-bug-description', + ISSUE_TYPE_BUG_COLOR: 'issue-type-bug-color', + + ISSUE_TYPE_HOTFIX: 'issue-type-hotfix', + ISSUE_TYPE_HOTFIX_DESCRIPTION: 'issue-type-hotfix-description', + ISSUE_TYPE_HOTFIX_COLOR: 'issue-type-hotfix-color', + + ISSUE_TYPE_FEATURE: 'issue-type-feature', + ISSUE_TYPE_FEATURE_DESCRIPTION: 'issue-type-feature-description', + ISSUE_TYPE_FEATURE_COLOR: 'issue-type-feature-color', + + ISSUE_TYPE_DOCUMENTATION: 'issue-type-documentation', + ISSUE_TYPE_DOCUMENTATION_DESCRIPTION: 'issue-type-documentation-description', + ISSUE_TYPE_DOCUMENTATION_COLOR: 'issue-type-documentation-color', + + ISSUE_TYPE_MAINTENANCE: 'issue-type-maintenance', + ISSUE_TYPE_MAINTENANCE_DESCRIPTION: 'issue-type-maintenance-description', + ISSUE_TYPE_MAINTENANCE_COLOR: 'issue-type-maintenance-color', + + ISSUE_TYPE_RELEASE: 'issue-type-release', + ISSUE_TYPE_RELEASE_DESCRIPTION: 'issue-type-release-description', + ISSUE_TYPE_RELEASE_COLOR: 'issue-type-release-color', + + ISSUE_TYPE_QUESTION: 'issue-type-question', + ISSUE_TYPE_QUESTION_DESCRIPTION: 'issue-type-question-description', + ISSUE_TYPE_QUESTION_COLOR: 'issue-type-question-color', + + ISSUE_TYPE_HELP: 'issue-type-help', + ISSUE_TYPE_HELP_DESCRIPTION: 'issue-type-help-description', + ISSUE_TYPE_HELP_COLOR: 'issue-type-help-color', + + ISSUE_TYPE_TASK: 'issue-type-task', + ISSUE_TYPE_TASK_DESCRIPTION: 'issue-type-task-description', + ISSUE_TYPE_TASK_COLOR: 'issue-type-task-color', + + // Locale + ISSUES_LOCALE: 'issues-locale', + PULL_REQUESTS_LOCALE: 'pull-requests-locale', + + // Size Thresholds + SIZE_XXL_THRESHOLD_LINES: 'size-xxl-threshold-lines', + SIZE_XXL_THRESHOLD_FILES: 'size-xxl-threshold-files', + SIZE_XXL_THRESHOLD_COMMITS: 'size-xxl-threshold-commits', + SIZE_XL_THRESHOLD_LINES: 'size-xl-threshold-lines', + SIZE_XL_THRESHOLD_FILES: 'size-xl-threshold-files', + SIZE_XL_THRESHOLD_COMMITS: 'size-xl-threshold-commits', + SIZE_L_THRESHOLD_LINES: 'size-l-threshold-lines', + SIZE_L_THRESHOLD_FILES: 'size-l-threshold-files', + SIZE_L_THRESHOLD_COMMITS: 'size-l-threshold-commits', + SIZE_M_THRESHOLD_LINES: 'size-m-threshold-lines', + SIZE_M_THRESHOLD_FILES: 'size-m-threshold-files', + SIZE_M_THRESHOLD_COMMITS: 'size-m-threshold-commits', + SIZE_S_THRESHOLD_LINES: 'size-s-threshold-lines', + SIZE_S_THRESHOLD_FILES: 'size-s-threshold-files', + SIZE_S_THRESHOLD_COMMITS: 'size-s-threshold-commits', + SIZE_XS_THRESHOLD_LINES: 'size-xs-threshold-lines', + SIZE_XS_THRESHOLD_FILES: 'size-xs-threshold-files', + SIZE_XS_THRESHOLD_COMMITS: 'size-xs-threshold-commits', + + // Branches + MAIN_BRANCH: 'main-branch', + DEVELOPMENT_BRANCH: 'development-branch', + FEATURE_TREE: 'feature-tree', + BUGFIX_TREE: 'bugfix-tree', + HOTFIX_TREE: 'hotfix-tree', + RELEASE_TREE: 'release-tree', + DOCS_TREE: 'docs-tree', + CHORE_TREE: 'chore-tree', + + // Commit + COMMIT_PREFIX_TRANSFORMS: 'commit-prefix-transforms', + + // Issue + BRANCH_MANAGEMENT_ALWAYS: 'branch-management-always', + REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', + DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', + + // Pull Request + PULL_REQUEST_DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', + PULL_REQUEST_DESIRED_REVIEWERS_COUNT: 'desired-reviewers-count', + PULL_REQUEST_MERGE_TIMEOUT: 'merge-timeout', + +} as const; diff --git a/src/application/contracts/product_identity.ts b/src/application/contracts/product_identity.ts new file mode 100644 index 00000000..f4cb39ad --- /dev/null +++ b/src/application/contracts/product_identity.ts @@ -0,0 +1 @@ +export const TITLE = 'Copilot'; diff --git a/src/application/policies/__tests__/setup_configuration_policy.test.ts b/src/application/policies/__tests__/setup_configuration_policy.test.ts index cbd73b54..8e4e6320 100644 --- a/src/application/policies/__tests__/setup_configuration_policy.test.ts +++ b/src/application/policies/__tests__/setup_configuration_policy.test.ts @@ -51,6 +51,23 @@ describe('setup configuration policy', () => { expect(plan.selectedFiles).toHaveLength(6); }); + it('keeps inactivity closure opt-in and wires its threshold when enabled', () => { + const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { + features: { inactiveIssueClosure: true }, + repository: { inactivityThresholdHours: 72 }, + }); + const plan = buildSetupPlan(configuration); + + expect(plan.workflowFiles).toContain('copilot_close_inactive_issues.yml'); + expect(buildSetupRepositoryVariables(configuration)).toEqual(expect.arrayContaining([ + { name: 'INACTIVITY_THRESHOLD_HOURS', value: '72' }, + ])); + expect(buildSetupActionInputs(configuration)['inactivity-threshold-hours']).toBe('72'); + expect(plan.warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Inactive issue closure is enabled'), + ])); + }); + it('supports independent agent runtime and model settings for every task', () => { const configuration = mergeSetupConfiguration(createDefaultSetupConfiguration(), { agents: { @@ -97,6 +114,7 @@ describe('setup configuration policy', () => { it('rejects invalid operational and agent values', () => { const configuration = createDefaultSetupConfiguration(); configuration.repository.desiredReviewersCount = 16; + configuration.repository.inactivityThresholdHours = 0; configuration.repository.mainBranch = 'main branch'; configuration.ai.bugbotCommentLimit = 0; configuration.agents.planner.model = 'unsafe model'; @@ -105,6 +123,7 @@ describe('setup configuration policy', () => { 'Desired reviewers must be between 0 and 15.', 'The main branch must be non-empty and contain no whitespace.', 'Bugbot comment limit must be between 1 and 100.', + 'Inactivity threshold must be between 1 and 8760 hours.', 'Model provider and model for planner cannot contain whitespace.', ])); }); diff --git a/src/application/policies/bugbot_constants.ts b/src/application/policies/bugbot_constants.ts new file mode 100644 index 00000000..61e2b9e4 --- /dev/null +++ b/src/application/policies/bugbot_constants.ts @@ -0,0 +1,8 @@ +/** Hidden marker prefix used to reconcile Bugbot findings across comments. */ +export const BUGBOT_MARKER_PREFIX = 'copilot-bugbot'; + +/** Maximum number of individual Bugbot comments published for one analysis. */ +export const BUGBOT_MAX_COMMENTS = 20; + +/** Minimum severity published by default. */ +export const BUGBOT_MIN_SEVERITY: 'info' | 'low' | 'medium' | 'high' = 'low'; diff --git a/src/application/policies/result_publication_policy.ts b/src/application/policies/result_publication_policy.ts index a7dce50c..46c3173e 100644 --- a/src/application/policies/result_publication_policy.ts +++ b/src/application/policies/result_publication_policy.ts @@ -21,7 +21,7 @@ const MAX_DEBUG_LOG_LENGTH = 12_000; /** Resolves the GitHub discussion that receives a result comment. */ export function resolveResultPublicationIssueNumber(input: ResultPublicationTargetInput): number | undefined { - if (input.isSingleAction) return input.singleActionIssue; + if (input.isSingleAction) return input.singleActionIssue > 0 ? input.singleActionIssue : undefined; if (input.isIssue) return input.issueNumber; if (input.isPullRequest) return input.pullRequestNumber; if (input.isPush && input.pushIssueNumber > 0) return input.pushIssueNumber; diff --git a/src/application/policies/setup_configuration_defaults.ts b/src/application/policies/setup_configuration_defaults.ts new file mode 100644 index 00000000..e527f4b2 --- /dev/null +++ b/src/application/policies/setup_configuration_defaults.ts @@ -0,0 +1,172 @@ +import type { AgentTask } from '../../domain/agent'; +import { + DEFAULT_AGENT_MODEL, + DEFAULT_AGENT_PROVIDER, + DEFAULT_MODEL_PROVIDER, +} from '../../domain/agent'; +import type { + SetupAgentConfiguration, + SetupAgentRoleConfiguration, + SetupConfiguration, + SetupFeatures, + SetupResourceStoragePolicy, + SetupStorageConfiguration, +} from '../../domain/setup'; +import { DEFAULT_INACTIVITY_THRESHOLD_HOURS } from '../../domain/issue_inactivity'; + +export const SETUP_AGENT_TASKS: readonly AgentTask[] = [ + 'planner', + 'findings', + 'reviewer', + 'fixer', + 'tester', + 'release', +]; + +export const SETUP_FEATURE_DESCRIPTIONS: Readonly> = { + issues: 'Issue automation: branching, labels, projects, and issue lifecycle', + pullRequests: 'Pull request automation: review, descriptions, and lifecycle', + commits: 'Commit automation: progress, sizing, and Bugbot analysis', + issueComments: 'Issue comments: questions, translations, and Bugbot autofix', + pullRequestComments: 'Pull request review comments: translations and Bugbot autofix', + release: 'Release workflow: versioning, changelog, tag, and GitHub Release', + hotfix: 'Hotfix workflow: emergency release from a production tag', + agentProvisioning: 'Agent CLI provisioning check workflow', + credentialHealth: 'Read-only remote credential health workflow for setup and doctor', + inactiveIssueClosure: 'Close issues after inactivity while waiting for an issuer or issue author', + issueTemplates: 'Issue templates for feature, bug, documentation, and operations', + pullRequestTemplate: 'Pull request template', +}; + +function defaultStoragePolicy(): SetupResourceStoragePolicy { + return { + defaultScope: 'repository', + organizationVisibility: 'selected', + preserveExisting: true, + overrides: {}, + }; +} + +export function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration { + return { + secrets: defaultStoragePolicy(), + variables: defaultStoragePolicy(), + }; +} + +export function createDefaultSetupConfiguration(): SetupConfiguration { + const defaultRole = (): SetupAgentRoleConfiguration => ({ + provider: DEFAULT_AGENT_PROVIDER, + modelProvider: DEFAULT_MODEL_PROVIDER, + model: DEFAULT_AGENT_MODEL, + effort: '', + }); + const agents = Object.fromEntries( + SETUP_AGENT_TASKS.map(task => [task, defaultRole()]), + ) as SetupAgentConfiguration; + const features: SetupFeatures = Object.fromEntries( + Object.keys(SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, feature !== 'inactiveIssueClosure']), + ); + return { + features, + agents, + repository: { + mainBranch: 'master', + developmentBranch: 'develop', + featureTree: 'feature', + bugfixTree: 'bugfix', + hotfixTree: 'hotfix', + releaseTree: 'release', + docsTree: 'docs', + choreTree: 'chore', + branchManagementAlways: false, + reopenIssueOnPush: true, + desiredAssigneesCount: 1, + desiredReviewersCount: 1, + mergeTimeout: 600, + inactivityThresholdHours: DEFAULT_INACTIVITY_THRESHOLD_HOURS, + issueLocale: 'en-US', + pullRequestLocale: 'en-US', + commitPrefixTransforms: 'replace-slash', + }, + ai: { + pullRequestDescription: true, + pullRequestDescriptionMode: 'replace', + ignoreFiles: 'build/*', + membersOnly: false, + includeReasoning: true, + bugbotSeverity: 'low', + bugbotCommentLimit: 20, + bugbotFixVerifyCommands: '', + provisioningMode: 'auto', + }, + projects: { + ids: '', + issueCreatedColumn: 'Todo', + pullRequestCreatedColumn: 'In Progress', + issueInProgressColumn: 'In Progress', + pullRequestInProgressColumn: 'In Progress', + }, + createInitialTag: true, + manageRepositoryVariables: true, + manageRepositorySecrets: true, + actionInputs: {}, + storage: createDefaultSetupStorageConfiguration(), + }; +} + +export type SetupConfigurationOverrides = { + features?: Partial; + agents?: Partial>>; + repository?: Partial; + ai?: Partial; + projects?: Partial; + createInitialTag?: boolean; + manageRepositoryVariables?: boolean; + manageRepositorySecrets?: boolean; + actionInputs?: Record; + storage?: { + secrets?: Partial; + variables?: Partial; + }; +}; + +export function mergeSetupConfiguration( + base: SetupConfiguration, + overrides: SetupConfigurationOverrides = {}, +): SetupConfiguration { + const agents = { ...base.agents } as SetupAgentConfiguration; + for (const task of SETUP_AGENT_TASKS) { + agents[task] = { ...base.agents[task], ...(overrides.agents?.[task] ?? {}) }; + } + return { + ...base, + features: { ...base.features, ...(overrides.features ?? {}) } as SetupFeatures, + agents, + repository: { ...base.repository, ...(overrides.repository ?? {}) }, + ai: { ...base.ai, ...(overrides.ai ?? {}) }, + projects: { ...base.projects, ...(overrides.projects ?? {}) }, + createInitialTag: overrides.createInitialTag ?? base.createInitialTag, + manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, + manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, + actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, + storage: { + secrets: { + ...base.storage.secrets, + ...(overrides.storage?.secrets ?? {}), + overrides: { + ...base.storage.secrets.overrides, + ...(overrides.storage?.secrets?.overrides ?? {}), + }, + }, + variables: { + ...base.storage.variables, + ...(overrides.storage?.variables ?? {}), + overrides: { + ...base.storage.variables.overrides, + ...(overrides.storage?.variables?.overrides ?? {}), + }, + }, + }, + }; +} diff --git a/src/application/policies/setup_configuration_plan.ts b/src/application/policies/setup_configuration_plan.ts new file mode 100644 index 00000000..150eaa0e --- /dev/null +++ b/src/application/policies/setup_configuration_plan.ts @@ -0,0 +1,234 @@ +import { normalizePullRequestDescriptionMode } from '../../domain/pull_request_description'; +import type { + SetupConfiguration, + SetupCredentialRequirement, + SetupPlan, + SetupVariable, +} from '../../domain/setup'; +import { SETUP_AGENT_TASKS } from './setup_configuration_defaults'; +import { usesOrganizationStorage } from './setup_configuration_storage_policy'; + +const WORKFLOW_FILES: Readonly> = { + issues: ['copilot_issue.yml'], + pullRequests: ['copilot_pull_request.yml'], + commits: ['copilot_commit.yml'], + issueComments: ['copilot_issue_comment.yml'], + pullRequestComments: ['copilot_pull_request_comment.yml'], + release: ['release_workflow.yml'], + hotfix: ['hotfix_workflow.yml'], + agentProvisioning: ['agent-cli-provisioning.yml'], + credentialHealth: ['copilot_credential_health.yml'], + inactiveIssueClosure: ['copilot_close_inactive_issues.yml'], +}; + +const ISSUE_TEMPLATE_FILES = [ + 'config.yml', + 'feature_request.yml', + 'bug_report.yml', + 'doc_update.yml', + 'chore_task.yml', + 'help_request.yml', + 'hotfix.yml', + 'release.yml', +]; + +const SECRET_BY_MODEL_PROVIDER: Readonly> = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + google: 'GOOGLE_API_KEY', + openrouter: 'OPENROUTER_API_KEY', +}; + +export function buildSetupPlan(configuration: SetupConfiguration): SetupPlan { + const workflowFiles = Object.entries(WORKFLOW_FILES) + .filter(([feature]) => configuration.features[feature] !== false) + .flatMap(([, files]) => files); + const issueTemplateFiles = configuration.features.issueTemplates === false + ? [] + : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') + .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); + const selectedFiles = [ + ...workflowFiles.map(file => `workflows/${file}`), + ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), + ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), + ]; + const credentialRequirements = buildSetupCredentialRequirements(configuration); + return { + configuration, + workflowFiles, + issueTemplateFiles, + selectedFiles, + variables: buildSetupRepositoryVariables(configuration), + requiredSecrets: credentialRequirements.map(requirement => requirement.name), + credentialRequirements, + warnings: buildSetupWarnings(configuration), + }; +} + +/** Builds the non-sensitive credential contract implied by the selected agents. */ +export function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[] { + const requirements = new Map(); + const add = (name: string, kind: SetupCredentialRequirement['kind'], description: string, provider?: string, model?: string) => { + if (!requirements.has(name)) requirements.set(name, { name, kind, description, provider, model }); + }; + add('PAT', 'workflowPat', 'A separate GitHub token owned by the bot account. It is used by workflows at runtime.'); + for (const task of SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (agent.provider === 'cursor') { + add('CURSOR_API_KEY', 'apiKey', 'Cursor API key used by the Cursor agent runtime.', 'cursor', agent.model); + continue; + } + if (agent.provider === 'opencode') add('OPENCODE_API_KEY', 'apiKey', 'OpenCode API key used by the OpenCode agent runtime.', 'opencode', agent.model); + if (agent.provider === 'codex') add('CODEX_ACCESS_TOKEN', 'apiKey', 'Codex access token used by the Codex agent runtime.', 'codex', agent.model); + const modelProvider = agent.modelProvider.trim().toLowerCase(); + if (modelProvider && !['local', 'ollama', 'lmstudio'].includes(modelProvider)) { + const name = SECRET_BY_MODEL_PROVIDER[modelProvider] ?? `${modelProvider.replace(/-/g, '_').toUpperCase()}_API_KEY`; + add(name, 'apiKey', `${modelProvider} API key for ${agent.model}.`, modelProvider, agent.model); + } + } + return [...requirements.values()]; +} + +export function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[] { + const variables: SetupVariable[] = []; + const add = (name: string, value: string | number | boolean | undefined) => { + if (value === undefined || value === '') return; + variables.push({ name, value: String(value) }); + }; + const base = configuration.agents.findings; + add('AGENT_PROVIDER', base.provider); + add('AGENT_MODEL_PROVIDER', base.modelProvider); + add('AGENT_MODEL', base.model); + add('AGENT_EFFORT', base.effort); + add('AGENT_PROVISIONING', configuration.ai.provisioningMode); + add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); + add('AGENT_ALLOWED_MODELS', unique(SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); + for (const task of SETUP_AGENT_TASKS) { + const prefix = task.toUpperCase(); + const agent = configuration.agents[task]; + add(`${prefix}_PROVIDER`, agent.provider); + add(`${prefix}_MODEL_PROVIDER`, agent.modelProvider); + add(`${prefix}_MODEL`, agent.model); + add(`${prefix}_EFFORT`, agent.effort); + } + const repository = configuration.repository; + add('MAIN_BRANCH', repository.mainBranch); + add('DEVELOPMENT_BRANCH', repository.developmentBranch); + add('FEATURE_TREE', repository.featureTree); + add('BUGFIX_TREE', repository.bugfixTree); + add('HOTFIX_TREE', repository.hotfixTree); + add('RELEASE_TREE', repository.releaseTree); + add('DOCS_TREE', repository.docsTree); + add('CHORE_TREE', repository.choreTree); + add('BRANCH_MANAGEMENT_ALWAYS', repository.branchManagementAlways); + add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); + add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); + add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); + add('MERGE_TIMEOUT', repository.mergeTimeout); + if (configuration.features.inactiveIssueClosure !== false) { + add('INACTIVITY_THRESHOLD_HOURS', repository.inactivityThresholdHours); + } + add('ISSUES_LOCALE', repository.issueLocale); + add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); + add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); + add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); + add('AI_PULL_REQUEST_DESCRIPTION_MODE', configuration.ai.pullRequestDescriptionMode); + add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); + add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); + add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); + add('BUGBOT_SEVERITY', configuration.ai.bugbotSeverity); + add('BUGBOT_COMMENT_LIMIT', configuration.ai.bugbotCommentLimit); + add('BUGBOT_AUTOFIX_VERIFY_COMMANDS', configuration.ai.bugbotFixVerifyCommands); + add('PROJECT_IDS', configuration.projects.ids); + add('PROJECT_COLUMN_ISSUE_CREATED', configuration.projects.issueCreatedColumn); + add('PROJECT_COLUMN_PULL_REQUEST_CREATED', configuration.projects.pullRequestCreatedColumn); + add('PROJECT_COLUMN_ISSUE_IN_PROGRESS', configuration.projects.issueInProgressColumn); + add('PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS', configuration.projects.pullRequestInProgressColumn); + return variables; +} + +export function buildSetupActionInputs(configuration: SetupConfiguration): Record { + const repository = configuration.repository; + const ai = configuration.ai; + const projects = configuration.projects; + return { + 'main-branch': repository.mainBranch, + 'development-branch': repository.developmentBranch, + 'feature-tree': repository.featureTree, + 'bugfix-tree': repository.bugfixTree, + 'hotfix-tree': repository.hotfixTree, + 'release-tree': repository.releaseTree, + 'docs-tree': repository.docsTree, + 'chore-tree': repository.choreTree, + 'branch-management-always': String(repository.branchManagementAlways), + 'reopen-issue-on-push': String(repository.reopenIssueOnPush), + 'desired-assignees-count': String(repository.desiredAssigneesCount), + 'desired-reviewers-count': String(repository.desiredReviewersCount), + 'merge-timeout': String(repository.mergeTimeout), + 'inactivity-threshold-hours': String(repository.inactivityThresholdHours), + 'issues-locale': repository.issueLocale, + 'pull-requests-locale': repository.pullRequestLocale, + 'commit-prefix-transforms': repository.commitPrefixTransforms, + 'ai-pull-request-description': String(ai.pullRequestDescription), + 'ai-pull-request-description-mode': normalizePullRequestDescriptionMode(ai.pullRequestDescriptionMode), + 'ai-ignore-files': ai.ignoreFiles, + 'ai-members-only': String(ai.membersOnly), + 'ai-include-reasoning': String(ai.includeReasoning), + 'bugbot-severity': ai.bugbotSeverity, + 'bugbot-comment-limit': String(ai.bugbotCommentLimit), + 'bugbot-fix-verify-commands': ai.bugbotFixVerifyCommands, + 'project-ids': projects.ids, + 'project-column-issue-created': projects.issueCreatedColumn, + 'project-column-pull-request-created': projects.pullRequestCreatedColumn, + 'project-column-issue-in-progress': projects.issueInProgressColumn, + 'project-column-pull-request-in-progress': projects.pullRequestInProgressColumn, + ...buildAgentActionInputs(configuration), + ...configuration.actionInputs, + }; +} + +function buildAgentActionInputs(configuration: SetupConfiguration): Record { + const result: Record = {}; + const base = configuration.agents.findings; + const add = (key: string, value: string | undefined) => { if (value !== undefined) result[key] = value; }; + add('agent-provider', base.provider); + add('agent-model-provider', base.modelProvider); + add('agent-model', base.model); + add('agent-effort', base.effort); + for (const task of SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + const prefix = `${task}-`; + add(`${prefix}provider`, agent.provider); + add(`${prefix}model-provider`, agent.modelProvider); + add(`${prefix}model`, agent.model); + add(`${prefix}effort`, agent.effort); + } + return result; +} + +function buildSetupWarnings(configuration: SetupConfiguration): string[] { + const warnings: string[] = []; + if (configuration.features.release !== false && configuration.features.hotfix !== false) { + warnings.push('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); + } + if (configuration.ai.provisioningMode === 'always') { + warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); + } + if (configuration.features.inactiveIssueClosure !== false) { + warnings.push('Inactive issue closure is enabled; waiting issues are closed after the configured inactivity threshold and can be reopened with a new comment.'); + } + if (configuration.projects.ids.trim()) { + warnings.push('Project IDs must be accessible to the PAT and use the expected project column names.'); + } + if (SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { + warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); + } + if (usesOrganizationStorage(configuration)) { + warnings.push('Organization-level Secrets and Variables require organization permissions; selected access is the safest default and repository values take precedence.'); + } + return warnings; +} + +function unique(values: string[]): string[] { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} diff --git a/src/application/policies/setup_configuration_policy.ts b/src/application/policies/setup_configuration_policy.ts index ef94cd21..b23fda05 100644 --- a/src/application/policies/setup_configuration_policy.ts +++ b/src/application/policies/setup_configuration_policy.ts @@ -1,583 +1,5 @@ -import type { AgentTask } from '../../domain/agent'; -import { - DEFAULT_AGENT_MODEL, - DEFAULT_AGENT_PROVIDER, - DEFAULT_MODEL_PROVIDER, -} from '../../domain/agent'; -import type { - SetupAgentConfiguration, - SetupAgentRoleConfiguration, - SetupConfiguration, - SetupFeatures, - SetupPlan, - SetupVariable, - SetupCredentialRequirement, - SetupResourceScope, - SetupResourceStoragePolicy, - SetupStorageConfiguration, - SetupRemoteConfiguration, - SetupResourceTarget, -} from '../../domain/setup'; -import { SUPPORTED_AGENT_PROVIDERS } from './agent_configuration_validation_policy'; -import { normalizePullRequestDescriptionMode } from '../../domain/pull_request_description'; - -export const SETUP_AGENT_TASKS: readonly AgentTask[] = [ - 'planner', - 'findings', - 'reviewer', - 'fixer', - 'tester', - 'release', -]; - -export const SETUP_FEATURE_DESCRIPTIONS: Readonly> = { - issues: 'Issue automation: branching, labels, projects, and issue lifecycle', - pullRequests: 'Pull request automation: review, descriptions, and lifecycle', - commits: 'Commit automation: progress, sizing, and Bugbot analysis', - issueComments: 'Issue comments: questions, translations, and Bugbot autofix', - pullRequestComments: 'Pull request review comments: translations and Bugbot autofix', - release: 'Release workflow: versioning, changelog, tag, and GitHub Release', - hotfix: 'Hotfix workflow: emergency release from a production tag', - agentProvisioning: 'Agent CLI provisioning check workflow', - credentialHealth: 'Read-only remote credential health workflow for setup and doctor', - issueTemplates: 'Issue templates for feature, bug, documentation, and operations', - pullRequestTemplate: 'Pull request template', -}; - -const WORKFLOW_FILES: Readonly> = { - issues: ['copilot_issue.yml'], - pullRequests: ['copilot_pull_request.yml'], - commits: ['copilot_commit.yml'], - issueComments: ['copilot_issue_comment.yml'], - pullRequestComments: ['copilot_pull_request_comment.yml'], - release: ['release_workflow.yml'], - hotfix: ['hotfix_workflow.yml'], - agentProvisioning: ['agent-cli-provisioning.yml'], - credentialHealth: ['copilot_credential_health.yml'], -}; - -const ISSUE_TEMPLATE_FILES = [ - 'config.yml', - 'feature_request.yml', - 'bug_report.yml', - 'doc_update.yml', - 'chore_task.yml', - 'help_request.yml', - 'hotfix.yml', - 'release.yml', -]; - -const SECRET_BY_MODEL_PROVIDER: Readonly> = { - openai: 'OPENAI_API_KEY', - anthropic: 'ANTHROPIC_API_KEY', - google: 'GOOGLE_API_KEY', - openrouter: 'OPENROUTER_API_KEY', -}; - -const RESOURCE_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/; - -function defaultStoragePolicy(): SetupResourceStoragePolicy { - return { - defaultScope: 'repository', - organizationVisibility: 'selected', - preserveExisting: true, - overrides: {}, - }; -} - -export function createDefaultSetupStorageConfiguration(): SetupStorageConfiguration { - return { - secrets: defaultStoragePolicy(), - variables: defaultStoragePolicy(), - }; -} - -export function createDefaultSetupConfiguration(): SetupConfiguration { - const defaultRole = (): SetupAgentRoleConfiguration => ({ - provider: DEFAULT_AGENT_PROVIDER, - modelProvider: DEFAULT_MODEL_PROVIDER, - model: DEFAULT_AGENT_MODEL, - effort: '', - }); - const agents = Object.fromEntries( - SETUP_AGENT_TASKS.map(task => [task, defaultRole()]), - ) as SetupAgentConfiguration; - const features: SetupFeatures = Object.fromEntries( - Object.keys(SETUP_FEATURE_DESCRIPTIONS).map(feature => [feature, true]), - ); - return { - features, - agents, - repository: { - mainBranch: 'master', - developmentBranch: 'develop', - featureTree: 'feature', - bugfixTree: 'bugfix', - hotfixTree: 'hotfix', - releaseTree: 'release', - docsTree: 'docs', - choreTree: 'chore', - branchManagementAlways: false, - reopenIssueOnPush: true, - desiredAssigneesCount: 1, - desiredReviewersCount: 1, - mergeTimeout: 600, - issueLocale: 'en-US', - pullRequestLocale: 'en-US', - commitPrefixTransforms: 'replace-slash', - }, - ai: { - pullRequestDescription: true, - pullRequestDescriptionMode: 'replace', - ignoreFiles: 'build/*', - membersOnly: false, - includeReasoning: true, - bugbotSeverity: 'low', - bugbotCommentLimit: 20, - bugbotFixVerifyCommands: '', - provisioningMode: 'auto', - }, - projects: { - ids: '', - issueCreatedColumn: 'Todo', - pullRequestCreatedColumn: 'In Progress', - issueInProgressColumn: 'In Progress', - pullRequestInProgressColumn: 'In Progress', - }, - createInitialTag: true, - manageRepositoryVariables: true, - manageRepositorySecrets: true, - actionInputs: {}, - storage: createDefaultSetupStorageConfiguration(), - }; -} - -export type SetupConfigurationOverrides = { - features?: Partial; - agents?: Partial>>; - repository?: Partial; - ai?: Partial; - projects?: Partial; - createInitialTag?: boolean; - manageRepositoryVariables?: boolean; - manageRepositorySecrets?: boolean; - actionInputs?: Record; - storage?: { - secrets?: Partial; - variables?: Partial; - }; -}; - -export function mergeSetupConfiguration( - base: SetupConfiguration, - overrides: SetupConfigurationOverrides = {}, -): SetupConfiguration { - const agents = { ...base.agents } as SetupAgentConfiguration; - for (const task of SETUP_AGENT_TASKS) { - agents[task] = { ...base.agents[task], ...(overrides.agents?.[task] ?? {}) }; - } - return { - ...base, - features: { ...base.features, ...(overrides.features ?? {}) } as SetupFeatures, - agents, - repository: { ...base.repository, ...(overrides.repository ?? {}) }, - ai: { ...base.ai, ...(overrides.ai ?? {}) }, - projects: { ...base.projects, ...(overrides.projects ?? {}) }, - createInitialTag: overrides.createInitialTag ?? base.createInitialTag, - manageRepositoryVariables: overrides.manageRepositoryVariables ?? base.manageRepositoryVariables, - manageRepositorySecrets: overrides.manageRepositorySecrets ?? base.manageRepositorySecrets, - actionInputs: { ...base.actionInputs, ...(overrides.actionInputs ?? {}) }, - storage: { - secrets: mergeStoragePolicy(base.storage?.secrets, overrides.storage?.secrets), - variables: mergeStoragePolicy(base.storage?.variables, overrides.storage?.variables), - }, - }; -} - -export function validateSetupConfiguration(configuration: SetupConfiguration): string[] { - const errors: string[] = []; - const nonEmpty = [ - ['main branch', configuration.repository.mainBranch], - ['development branch', configuration.repository.developmentBranch], - ['feature branch prefix', configuration.repository.featureTree], - ['bugfix branch prefix', configuration.repository.bugfixTree], - ['hotfix branch prefix', configuration.repository.hotfixTree], - ['release branch prefix', configuration.repository.releaseTree], - ['docs branch prefix', configuration.repository.docsTree], - ['chore branch prefix', configuration.repository.choreTree], - ] as const; - for (const [name, value] of nonEmpty) { - if (!value.trim() || /\s/.test(value)) errors.push(`The ${name} must be non-empty and contain no whitespace.`); - } - if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { - errors.push('Desired assignees must be between 0 and 10.'); - } - if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { - errors.push('Desired reviewers must be between 0 and 15.'); - } - if (configuration.repository.mergeTimeout < 0) errors.push('Merge timeout cannot be negative.'); - if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { - errors.push('Bugbot comment limit must be between 1 and 100.'); - } - if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { - errors.push('Bugbot severity must be info, low, medium, or high.'); - } - if (configuration.ai.pullRequestDescriptionMode !== undefined - && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { - errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); - } - if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { - errors.push('Agent provisioning must be auto, always, or disabled.'); - } - errors.push(...validateStorageConfiguration(configuration.storage)); - for (const task of SETUP_AGENT_TASKS) { - const agent = configuration.agents[task]; - if (!SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); - if (!agent.modelProvider.trim() || !agent.model.trim()) errors.push(`Model provider and model are required for ${task}.`); - if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) errors.push(`Model provider and model for ${task} cannot contain whitespace.`); - } - return errors; -} - -export function buildSetupPlan(configuration: SetupConfiguration): SetupPlan { - const workflowFiles = Object.entries(WORKFLOW_FILES) - .filter(([feature]) => configuration.features[feature] !== false) - .flatMap(([, files]) => files); - const issueTemplateFiles = configuration.features.issueTemplates === false - ? [] - : ISSUE_TEMPLATE_FILES.filter(file => configuration.features.release !== false || file !== 'release.yml') - .filter(file => configuration.features.hotfix !== false || file !== 'hotfix.yml'); - const selectedFiles = [ - ...workflowFiles.map(file => `workflows/${file}`), - ...issueTemplateFiles.map(file => `ISSUE_TEMPLATE/${file}`), - ...(configuration.features.pullRequestTemplate === false ? [] : ['pull_request_template.md']), - ]; - return { - configuration, - workflowFiles, - issueTemplateFiles, - selectedFiles, - variables: buildSetupRepositoryVariables(configuration), - requiredSecrets: buildRequiredSetupSecrets(configuration), - credentialRequirements: buildSetupCredentialRequirements(configuration), - warnings: buildSetupWarnings(configuration), - }; -} - -/** Builds the non-sensitive credential contract implied by the selected agents. */ -export function buildSetupCredentialRequirements(configuration: SetupConfiguration): SetupCredentialRequirement[] { - const requirements = new Map(); - const add = (name: string, kind: SetupCredentialRequirement['kind'], description: string, provider?: string, model?: string) => { - if (!requirements.has(name)) requirements.set(name, { name, kind, description, provider, model }); - }; - add('PAT', 'workflowPat', 'A separate GitHub token owned by the bot account. It is used by workflows at runtime.'); - for (const task of SETUP_AGENT_TASKS) { - const agent = configuration.agents[task]; - if (agent.provider === 'cursor') { - add('CURSOR_API_KEY', 'apiKey', 'Cursor API key used by the Cursor agent runtime.', 'cursor', agent.model); - continue; - } - if (agent.provider === 'opencode') add('OPENCODE_API_KEY', 'apiKey', 'OpenCode API key used by the OpenCode agent runtime.', 'opencode', agent.model); - if (agent.provider === 'codex') add('CODEX_ACCESS_TOKEN', 'apiKey', 'Codex access token used by the Codex agent runtime.', 'codex', agent.model); - const modelProvider = agent.modelProvider.trim().toLowerCase(); - if (modelProvider && !['local', 'ollama', 'lmstudio'].includes(modelProvider)) { - const name = SECRET_BY_MODEL_PROVIDER[modelProvider] ?? `${modelProvider.replace(/-/g, '_').toUpperCase()}_API_KEY`; - add(name, 'apiKey', `${modelProvider} API key for ${agent.model}.`, modelProvider, agent.model); - } - } - return [...requirements.values()]; -} - -export function buildSetupRepositoryVariables(configuration: SetupConfiguration): SetupVariable[] { - const variables: SetupVariable[] = []; - const add = (name: string, value: string | number | boolean | undefined) => { - if (value === undefined || value === '') return; - variables.push({ name, value: String(value) }); - }; - const base = configuration.agents.findings; - add('AGENT_PROVIDER', base.provider); - add('AGENT_MODEL_PROVIDER', base.modelProvider); - add('AGENT_MODEL', base.model); - add('AGENT_EFFORT', base.effort); - add('AGENT_PROVISIONING', configuration.ai.provisioningMode); - add('AGENT_ALLOWED_MODEL_PROVIDERS', unique(SETUP_AGENT_TASKS.map(task => configuration.agents[task].modelProvider)).join(',')); - add('AGENT_ALLOWED_MODELS', unique(SETUP_AGENT_TASKS.map(task => `${configuration.agents[task].modelProvider}/${configuration.agents[task].model}`)).join(',')); - - for (const task of SETUP_AGENT_TASKS) { - const prefix = task.toUpperCase(); - const agent = configuration.agents[task]; - add(`${prefix}_PROVIDER`, agent.provider); - add(`${prefix}_MODEL_PROVIDER`, agent.modelProvider); - add(`${prefix}_MODEL`, agent.model); - add(`${prefix}_EFFORT`, agent.effort); - } - - const repository = configuration.repository; - add('MAIN_BRANCH', repository.mainBranch); - add('DEVELOPMENT_BRANCH', repository.developmentBranch); - add('FEATURE_TREE', repository.featureTree); - add('BUGFIX_TREE', repository.bugfixTree); - add('HOTFIX_TREE', repository.hotfixTree); - add('RELEASE_TREE', repository.releaseTree); - add('DOCS_TREE', repository.docsTree); - add('CHORE_TREE', repository.choreTree); - add('BRANCH_MANAGEMENT_ALWAYS', repository.branchManagementAlways); - add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); - add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); - add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); - add('MERGE_TIMEOUT', repository.mergeTimeout); - add('ISSUES_LOCALE', repository.issueLocale); - add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); - add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); - - add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); - add('AI_PULL_REQUEST_DESCRIPTION_MODE', configuration.ai.pullRequestDescriptionMode); - add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); - add('AI_MEMBERS_ONLY', configuration.ai.membersOnly); - add('AI_INCLUDE_REASONING', configuration.ai.includeReasoning); - add('BUGBOT_SEVERITY', configuration.ai.bugbotSeverity); - add('BUGBOT_COMMENT_LIMIT', configuration.ai.bugbotCommentLimit); - add('BUGBOT_AUTOFIX_VERIFY_COMMANDS', configuration.ai.bugbotFixVerifyCommands); - - add('PROJECT_IDS', configuration.projects.ids); - add('PROJECT_COLUMN_ISSUE_CREATED', configuration.projects.issueCreatedColumn); - add('PROJECT_COLUMN_PULL_REQUEST_CREATED', configuration.projects.pullRequestCreatedColumn); - add('PROJECT_COLUMN_ISSUE_IN_PROGRESS', configuration.projects.issueInProgressColumn); - add('PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS', configuration.projects.pullRequestInProgressColumn); - return variables; -} - -export function buildSetupActionInputs(configuration: SetupConfiguration): Record { - const repository = configuration.repository; - const ai = configuration.ai; - const projects = configuration.projects; - return { - 'main-branch': repository.mainBranch, - 'development-branch': repository.developmentBranch, - 'feature-tree': repository.featureTree, - 'bugfix-tree': repository.bugfixTree, - 'hotfix-tree': repository.hotfixTree, - 'release-tree': repository.releaseTree, - 'docs-tree': repository.docsTree, - 'chore-tree': repository.choreTree, - 'branch-management-always': String(repository.branchManagementAlways), - 'reopen-issue-on-push': String(repository.reopenIssueOnPush), - 'desired-assignees-count': String(repository.desiredAssigneesCount), - 'desired-reviewers-count': String(repository.desiredReviewersCount), - 'merge-timeout': String(repository.mergeTimeout), - 'issues-locale': repository.issueLocale, - 'pull-requests-locale': repository.pullRequestLocale, - 'commit-prefix-transforms': repository.commitPrefixTransforms, - 'ai-pull-request-description': String(ai.pullRequestDescription), - 'ai-pull-request-description-mode': normalizePullRequestDescriptionMode(ai.pullRequestDescriptionMode), - 'ai-ignore-files': ai.ignoreFiles, - 'ai-members-only': String(ai.membersOnly), - 'ai-include-reasoning': String(ai.includeReasoning), - 'bugbot-severity': ai.bugbotSeverity, - 'bugbot-comment-limit': String(ai.bugbotCommentLimit), - 'bugbot-fix-verify-commands': ai.bugbotFixVerifyCommands, - 'project-ids': projects.ids, - 'project-column-issue-created': projects.issueCreatedColumn, - 'project-column-pull-request-created': projects.pullRequestCreatedColumn, - 'project-column-issue-in-progress': projects.issueInProgressColumn, - 'project-column-pull-request-in-progress': projects.pullRequestInProgressColumn, - ...buildAgentActionInputs(configuration), - ...configuration.actionInputs, - }; -} - -function buildAgentActionInputs(configuration: SetupConfiguration): Record { - const result: Record = {}; - const base = configuration.agents.findings; - const add = (key: string, value: string | undefined) => { if (value !== undefined) result[key] = value; }; - add('agent-provider', base.provider); - add('agent-model-provider', base.modelProvider); - add('agent-model', base.model); - add('agent-effort', base.effort); - for (const task of SETUP_AGENT_TASKS) { - const agent = configuration.agents[task]; - const prefix = `${task}-`; - add(`${prefix}provider`, agent.provider); - add(`${prefix}model-provider`, agent.modelProvider); - add(`${prefix}model`, agent.model); - add(`${prefix}effort`, agent.effort); - } - return result; -} - -function buildRequiredSetupSecrets(configuration: SetupConfiguration): string[] { - return buildSetupCredentialRequirements(configuration).map(requirement => requirement.name); -} - -function buildSetupWarnings(configuration: SetupConfiguration): string[] { - const warnings: string[] = []; - if (configuration.features.release !== false && configuration.features.hotfix !== false) { - warnings.push('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); - } - if (configuration.ai.provisioningMode === 'always') { - warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); - } - if (configuration.projects.ids.trim()) { - warnings.push('Project IDs must be accessible to the PAT and use the expected project column names.'); - } - if (SETUP_AGENT_TASKS.some(task => configuration.agents[task].provider === 'cursor')) { - warnings.push('Cursor is an experimental runtime in Copilot and requires a verified installer checksum plus CURSOR_API_KEY.'); - } - if (usesOrganizationStorage(configuration)) { - warnings.push('Organization-level Secrets and Variables require organization permissions; selected access is the safest default and repository values take precedence.'); - } - return warnings; -} - -export function resolveSetupResourceScope( - policy: SetupResourceStoragePolicy, - name: string, -): SetupResourceScope { - return policy.overrides[name] ?? policy.defaultScope; -} - -export type SetupResourceKind = 'secret' | 'variable'; - -export function getSetupResourceStoragePolicy( - configuration: SetupConfiguration, - kind: SetupResourceKind, -): SetupResourceStoragePolicy { - return getSetupStorageConfiguration(configuration)[kind === 'secret' ? 'secrets' : 'variables']; -} - -export function getSetupStorageConfiguration(configuration: Pick): SetupStorageConfiguration { - const fallback = createDefaultSetupStorageConfiguration(); - return { - secrets: mergeStoragePolicy(fallback.secrets, configuration.storage?.secrets), - variables: mergeStoragePolicy(fallback.variables, configuration.storage?.variables), - }; -} - -export function resolveSetupResourceTarget( - configuration: SetupConfiguration, - kind: SetupResourceKind, - name: string, - remote?: SetupRemoteConfiguration, -): SetupResourceTarget { - const policy = getSetupResourceStoragePolicy(configuration, kind); - const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); - const existingScope = setupResourceExists(remote, kind, name).effective; - const scope = existingScope && policy.preserveExisting && !explicitOverride - ? existingScope - : resolveSetupResourceScope(policy, name); - return { - scope, - organizationVisibility: policy.organizationVisibility, - repositoryId: remote?.repositoryId, - }; -} - -export function setupResourceExists( - remote: SetupRemoteConfiguration | undefined, - kind: SetupResourceKind, - name: string, -): { repository: boolean; organization: boolean; effective?: SetupResourceScope } { - if (!remote) return { repository: false, organization: false }; - const repository = kind === 'secret' - ? remote.repositorySecrets.includes(name) - : remote.repositoryVariables.some(variable => variable.name === name); - const organizationAccess = kind === 'secret' - ? (remote.organizationSecretsAccess ?? remote.organizationAccess) - : (remote.organizationVariablesAccess ?? remote.organizationAccess); - const organization = organizationAccess === 'available' && (kind === 'secret' - ? remote.organizationSecrets.includes(name) - : remote.organizationVariables.some(variable => variable.name === name)); - return { - repository, - organization, - effective: repository ? 'repository' : organization ? 'organization' : undefined, - }; -} - -export function shouldUpsertSetupResource( - configuration: SetupConfiguration, - kind: SetupResourceKind, - name: string, - remote?: SetupRemoteConfiguration, -): boolean { - const policy = getSetupResourceStoragePolicy(configuration, kind); - const state = setupResourceExists(remote, kind, name); - if (!state.effective) return true; - const requested = resolveSetupResourceScope(policy, name); - const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); - return requested === state.effective || explicitOverride || !policy.preserveExisting; -} - -export function validateSetupStorageAgainstRemote( - configuration: SetupConfiguration, - remote: SetupRemoteConfiguration, -): string[] { - const errors: string[] = []; - const policies: Array<[SetupResourceKind, SetupResourceStoragePolicy, boolean]> = [ - ['secret', getSetupResourceStoragePolicy(configuration, 'secret'), configuration.manageRepositorySecrets], - ['variable', getSetupResourceStoragePolicy(configuration, 'variable'), configuration.manageRepositoryVariables], - ]; - for (const [kind, policy, managed] of policies) { - if (!managed) continue; - const needsOrganization = policy.defaultScope === 'organization' - || Object.values(policy.overrides).includes('organization'); - if (!needsOrganization) continue; - if (remote.ownerType !== 'Organization') { - errors.push(`Organization-level ${kind} storage is only available for organization-owned repositories.`); - continue; - } - const access = kind === 'secret' ? remote.organizationSecretsAccess : remote.organizationVariablesAccess; - if (access !== 'available') { - errors.push(`The setup PAT cannot inspect organization ${kind}s for this repository. Organization ${kind} permissions are required.`); - } - if (policy.organizationVisibility === 'selected' && remote.repositoryId === undefined) { - errors.push(`The repository ID is required for selected organization ${kind} access.`); - } - } - return errors; -} - -export function usesOrganizationStorage(configuration: SetupConfiguration): boolean { - const storage = getSetupStorageConfiguration(configuration); - return [storage.secrets, storage.variables].some(policy => - policy.defaultScope === 'organization' || Object.values(policy.overrides).includes('organization'), - ); -} - -function mergeStoragePolicy( - base: SetupResourceStoragePolicy | undefined, - override: Partial | undefined, -): SetupResourceStoragePolicy { - const fallback = base ?? defaultStoragePolicy(); - return { - ...fallback, - ...(override ?? {}), - overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, - }; -} - -function validateStorageConfiguration(storage: SetupStorageConfiguration | undefined): string[] { - // Setup files created before scoped storage was introduced remain valid and - // receive the repository-level defaults through getSetupStorageConfiguration. - if (!storage) return []; - const errors: string[] = []; - for (const [kind, policy] of Object.entries(storage)) { - if (!policy || !['repository', 'organization'].includes(policy.defaultScope)) { - errors.push(`${kind} default scope must be repository or organization.`); - continue; - } - if (!['all', 'private', 'selected'].includes(policy.organizationVisibility)) { - errors.push(`${kind} organization visibility must be all, private, or selected.`); - } - if (typeof policy.preserveExisting !== 'boolean') errors.push(`${kind} preserveExisting must be a boolean.`); - for (const [name, scope] of Object.entries(policy.overrides ?? {}) as [string, SetupResourceScope][]) { - if (!RESOURCE_NAME_PATTERN.test(name)) errors.push(`${kind} override name ${name} must be an uppercase GitHub Actions name.`); - if (!['repository', 'organization'].includes(scope)) errors.push(`${kind} override ${name} must use repository or organization.`); - } - } - return errors; -} - -function unique(values: string[]): string[] { - return [...new Set(values.map(value => value.trim()).filter(Boolean))]; -} +/** Public setup-policy boundary. Each concern is implemented in a focused policy module. */ +export * from './setup_configuration_defaults'; +export * from './setup_configuration_plan'; +export * from './setup_configuration_storage_policy'; +export * from './setup_configuration_validation'; diff --git a/src/application/policies/setup_configuration_storage_policy.ts b/src/application/policies/setup_configuration_storage_policy.ts new file mode 100644 index 00000000..d517c513 --- /dev/null +++ b/src/application/policies/setup_configuration_storage_policy.ts @@ -0,0 +1,158 @@ +import type { + SetupConfiguration, + SetupRemoteConfiguration, + SetupResourceScope, + SetupResourceStoragePolicy, + SetupResourceTarget, + SetupStorageConfiguration, +} from '../../domain/setup'; +import { createDefaultSetupStorageConfiguration } from './setup_configuration_defaults'; + +export type SetupResourceKind = 'secret' | 'variable'; + +export function resolveSetupResourceScope( + policy: SetupResourceStoragePolicy, + name: string, +): SetupResourceScope { + return policy.overrides[name] ?? policy.defaultScope; +} + +export function getSetupResourceStoragePolicy( + configuration: SetupConfiguration, + kind: SetupResourceKind, +): SetupResourceStoragePolicy { + return getSetupStorageConfiguration(configuration)[kind === 'secret' ? 'secrets' : 'variables']; +} + +export function getSetupStorageConfiguration( + configuration: Pick, +): SetupStorageConfiguration { + const fallback = createDefaultSetupStorageConfiguration(); + return { + secrets: mergeStoragePolicy(fallback.secrets, configuration.storage?.secrets), + variables: mergeStoragePolicy(fallback.variables, configuration.storage?.variables), + }; +} + +export function resolveSetupResourceTarget( + configuration: SetupConfiguration, + kind: SetupResourceKind, + name: string, + remote?: SetupRemoteConfiguration, +): SetupResourceTarget { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + const existingScope = setupResourceExists(remote, kind, name).effective; + const scope = existingScope && policy.preserveExisting && !explicitOverride + ? existingScope + : resolveSetupResourceScope(policy, name); + return { + scope, + organizationVisibility: policy.organizationVisibility, + repositoryId: remote?.repositoryId, + }; +} + +export function setupResourceExists( + remote: SetupRemoteConfiguration | undefined, + kind: SetupResourceKind, + name: string, +): { repository: boolean; organization: boolean; effective?: SetupResourceScope } { + if (!remote) return { repository: false, organization: false }; + const repository = kind === 'secret' + ? remote.repositorySecrets.includes(name) + : remote.repositoryVariables.some(variable => variable.name === name); + const organizationAccess = kind === 'secret' + ? (remote.organizationSecretsAccess ?? remote.organizationAccess) + : (remote.organizationVariablesAccess ?? remote.organizationAccess); + const organization = organizationAccess === 'available' && (kind === 'secret' + ? remote.organizationSecrets.includes(name) + : remote.organizationVariables.some(variable => variable.name === name)); + return { + repository, + organization, + effective: repository ? 'repository' : organization ? 'organization' : undefined, + }; +} + +export function shouldUpsertSetupResource( + configuration: SetupConfiguration, + kind: SetupResourceKind, + name: string, + remote?: SetupRemoteConfiguration, +): boolean { + const policy = getSetupResourceStoragePolicy(configuration, kind); + const state = setupResourceExists(remote, kind, name); + if (!state.effective) return true; + const requested = resolveSetupResourceScope(policy, name); + const explicitOverride = Object.prototype.hasOwnProperty.call(policy.overrides, name); + return requested === state.effective || explicitOverride || !policy.preserveExisting; +} + +export function validateSetupStorageAgainstRemote( + configuration: SetupConfiguration, + remote: SetupRemoteConfiguration, +): string[] { + const errors: string[] = []; + const policies: Array<[SetupResourceKind, SetupResourceStoragePolicy, boolean]> = [ + ['secret', getSetupResourceStoragePolicy(configuration, 'secret'), configuration.manageRepositorySecrets], + ['variable', getSetupResourceStoragePolicy(configuration, 'variable'), configuration.manageRepositoryVariables], + ]; + for (const [kind, policy, managed] of policies) { + if (!managed) continue; + const needsOrganization = policy.defaultScope === 'organization' + || Object.values(policy.overrides).includes('organization'); + if (!needsOrganization) continue; + if (remote.ownerType !== 'Organization') { + errors.push(`Organization-level ${kind} storage is only available for organization-owned repositories.`); + continue; + } + const access = kind === 'secret' ? remote.organizationSecretsAccess : remote.organizationVariablesAccess; + if (access !== 'available') { + errors.push(`The setup PAT cannot inspect organization ${kind}s for this repository. Organization ${kind} permissions are required.`); + } + if (policy.organizationVisibility === 'selected' && remote.repositoryId === undefined) { + errors.push(`The repository ID is required for selected organization ${kind} access.`); + } + } + return errors; +} + +export function usesOrganizationStorage(configuration: SetupConfiguration): boolean { + const storage = getSetupStorageConfiguration(configuration); + return [storage.secrets, storage.variables].some(policy => + policy.defaultScope === 'organization' || Object.values(policy.overrides).includes('organization'), + ); +} + +export function validateStorageConfiguration(storage: SetupStorageConfiguration | undefined): string[] { + if (!storage) return []; + const errors: string[] = []; + for (const [kind, policy] of Object.entries(storage)) { + if (!policy || !['repository', 'organization'].includes(policy.defaultScope)) { + errors.push(`${kind} default scope must be repository or organization.`); + continue; + } + if (!['all', 'private', 'selected'].includes(policy.organizationVisibility)) { + errors.push(`${kind} organization visibility must be all, private, or selected.`); + } + if (typeof policy.preserveExisting !== 'boolean') errors.push(`${kind} preserveExisting must be a boolean.`); + for (const [name, scope] of Object.entries(policy.overrides ?? {}) as [string, SetupResourceScope][]) { + if (!/^[A-Z][A-Z0-9_]*$/.test(name)) errors.push(`${kind} override name ${name} must be an uppercase GitHub Actions name.`); + if (!['repository', 'organization'].includes(scope)) errors.push(`${kind} override ${name} must use repository or organization.`); + } + } + return errors; +} + +function mergeStoragePolicy( + base: SetupResourceStoragePolicy | undefined, + override: Partial | undefined, +): SetupResourceStoragePolicy { + const fallback = base ?? createDefaultSetupStorageConfiguration().secrets; + return { + ...fallback, + ...(override ?? {}), + overrides: { ...fallback.overrides, ...(override?.overrides ?? {}) }, + }; +} diff --git a/src/application/policies/setup_configuration_validation.ts b/src/application/policies/setup_configuration_validation.ts new file mode 100644 index 00000000..7701bf06 --- /dev/null +++ b/src/application/policies/setup_configuration_validation.ts @@ -0,0 +1,55 @@ +import type { SetupConfiguration } from '../../domain/setup'; +import { SETUP_AGENT_TASKS } from './setup_configuration_defaults'; +import { SUPPORTED_AGENT_PROVIDERS } from './agent_configuration_validation_policy'; +import { validateStorageConfiguration } from './setup_configuration_storage_policy'; +import { MAX_INACTIVITY_THRESHOLD_HOURS } from '../../domain/issue_inactivity'; + +export function validateSetupConfiguration(configuration: SetupConfiguration): string[] { + const errors: string[] = []; + const nonEmpty = [ + ['main branch', configuration.repository.mainBranch], + ['development branch', configuration.repository.developmentBranch], + ['feature branch prefix', configuration.repository.featureTree], + ['bugfix branch prefix', configuration.repository.bugfixTree], + ['hotfix branch prefix', configuration.repository.hotfixTree], + ['release branch prefix', configuration.repository.releaseTree], + ['docs branch prefix', configuration.repository.docsTree], + ['chore branch prefix', configuration.repository.choreTree], + ] as const; + for (const [name, value] of nonEmpty) { + if (!value.trim() || /\s/.test(value)) errors.push(`The ${name} must be non-empty and contain no whitespace.`); + } + if (configuration.repository.desiredAssigneesCount < 0 || configuration.repository.desiredAssigneesCount > 10) { + errors.push('Desired assignees must be between 0 and 10.'); + } + if (configuration.repository.desiredReviewersCount < 0 || configuration.repository.desiredReviewersCount > 15) { + errors.push('Desired reviewers must be between 0 and 15.'); + } + if (configuration.repository.mergeTimeout < 0) errors.push('Merge timeout cannot be negative.'); + if (!Number.isInteger(configuration.repository.inactivityThresholdHours) + || configuration.repository.inactivityThresholdHours < 1 + || configuration.repository.inactivityThresholdHours > MAX_INACTIVITY_THRESHOLD_HOURS) { + errors.push(`Inactivity threshold must be between 1 and ${MAX_INACTIVITY_THRESHOLD_HOURS} hours.`); + } + if (configuration.ai.bugbotCommentLimit < 1 || configuration.ai.bugbotCommentLimit > 100) { + errors.push('Bugbot comment limit must be between 1 and 100.'); + } + if (!['info', 'low', 'medium', 'high'].includes(configuration.ai.bugbotSeverity)) { + errors.push('Bugbot severity must be info, low, medium, or high.'); + } + if (configuration.ai.pullRequestDescriptionMode !== undefined + && !['replace', 'append', 'preserve', 'disabled'].includes(configuration.ai.pullRequestDescriptionMode)) { + errors.push('Pull-request description mode must be replace, append, preserve, or disabled.'); + } + if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { + errors.push('Agent provisioning must be auto, always, or disabled.'); + } + errors.push(...validateStorageConfiguration(configuration.storage)); + for (const task of SETUP_AGENT_TASKS) { + const agent = configuration.agents[task]; + if (!SUPPORTED_AGENT_PROVIDERS.includes(agent.provider)) errors.push(`Unsupported provider for ${task}: ${agent.provider}.`); + if (!agent.modelProvider.trim() || !agent.model.trim()) errors.push(`Model provider and model are required for ${task}.`); + if (/\s/.test(agent.model) || /\s/.test(agent.modelProvider)) errors.push(`Model provider and model for ${task} cannot contain whitespace.`); + } + return errors; +} diff --git a/src/application/policies/workflow_queue_policy.ts b/src/application/policies/workflow_queue_policy.ts index 6cf6d870..bd8d9a9f 100644 --- a/src/application/policies/workflow_queue_policy.ts +++ b/src/application/policies/workflow_queue_policy.ts @@ -9,6 +9,7 @@ export const COPILOT_WORKFLOW_NAMES = [ 'Copilot - Commit', 'Copilot - Pull Request', 'Copilot - Pull Request Comment', + 'Copilot - Close Inactive Issues', 'Task - Hotfix', 'Task - Release', ] as const; diff --git a/src/application/ports/issue_inactivity_ports.ts b/src/application/ports/issue_inactivity_ports.ts new file mode 100644 index 00000000..2415d532 --- /dev/null +++ b/src/application/ports/issue_inactivity_ports.ts @@ -0,0 +1,20 @@ +import type { IssueActivitySnapshot } from '../../domain/issue_inactivity'; + +export interface IssueInactivityQueryPort { + listOpenIssuesByLabel( + owner: string, + repository: string, + label: string, + token: string, + ): Promise; + getOpenIssue( + owner: string, + repository: string, + issueNumber: number, + token: string, + ): Promise; +} + +export interface IssueInactivityClockPort { + nowMilliseconds(): number; +} diff --git a/src/application/usecases/__tests__/single_action_use_case.test.ts b/src/application/usecases/__tests__/single_action_use_case.test.ts index 927f14ce..cfd8e391 100644 --- a/src/application/usecases/__tests__/single_action_use_case.test.ts +++ b/src/application/usecases/__tests__/single_action_use_case.test.ts @@ -1,7 +1,7 @@ import { SingleActionUseCase } from '../single_action_use_case'; import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; -import { ACTIONS } from '../../../utils/constants'; +import { ACTIONS } from '../../../data/model/action_types'; jest.mock('../../../utils/logger', () => ({ logInfo: jest.fn(), @@ -62,6 +62,7 @@ function minimalExecution(singleAction: { isCheckProgressAction?: boolean; isDetectPotentialProblemsAction?: boolean; isRecommendStepsAction?: boolean; + isCloseInactiveIssuesAction?: boolean; }): Execution { return { singleAction: { @@ -94,6 +95,9 @@ function minimalExecution(singleAction: { get isRecommendStepsAction() { return singleAction.isRecommendStepsAction ?? this.currentSingleAction === ACTIONS.RECOMMEND_STEPS; }, + get isCloseInactiveIssuesAction() { + return singleAction.isCloseInactiveIssuesAction ?? this.currentSingleAction === ACTIONS.CLOSE_INACTIVE_ISSUES; + }, } as Execution['singleAction'], } as Execution; } @@ -136,6 +140,30 @@ describe('SingleActionUseCase', () => { expect(results).toEqual([r]); }); + it('dispatches to CloseInactiveIssuesUseCase when action is close_inactive_issues_action', async () => { + const closeInactiveInvoke = jest.fn().mockResolvedValue([]); + const useCase = new SingleActionUseCase( + { invoke: jest.fn().mockResolvedValue([]) } as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + { invoke: closeInactiveInvoke } as any, + ); + const param = minimalExecution({ + validSingleAction: true, + currentSingleAction: ACTIONS.CLOSE_INACTIVE_ISSUES, + }); + + await useCase.invoke(param); + + expect(closeInactiveInvoke).toHaveBeenCalledWith(param); + }); + it('dispatches to CheckProgressUseCase when action is check_progress', async () => { mockCheckProgressInvoke.mockResolvedValue([ new Result({ id: 'cp', success: true, executed: true, steps: [] }), diff --git a/src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts b/src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts new file mode 100644 index 00000000..c767fc5f --- /dev/null +++ b/src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts @@ -0,0 +1,142 @@ +import { CloseInactiveIssuesUseCase } from '../close_inactive_issues_use_case'; +import type { Execution } from '../../../../data/model/execution'; +import type { IssueActivitySnapshot } from '../../../../domain/issue_inactivity'; + +jest.mock('../../../../utils/logger', () => ({ + logInfo: jest.fn(), + logDebugInfo: jest.fn(), + logError: jest.fn(), +})); + +function execution(overrides: Record = {}): Execution { + return { + owner: 'owner', + repo: 'repo', + tokens: { token: 'token' }, + inactivityThresholdHours: 168, + labels: { + lifecycle: { + awaitingMaintainer: 'state:awaiting-maintainer', + awaitingIssueAuthor: 'state:awaiting-issue-author', + aiProcessing: 'state:ai-processing', + }, + }, + ...overrides, + } as unknown as Execution; +} + +function snapshot(overrides: Partial = {}): IssueActivitySnapshot { + return { + number: 42, + updatedAt: '2026-08-28T00:00:00.000Z', + isPullRequest: false, + labels: ['state:awaiting-maintainer'], + ...overrides, + }; +} + +describe('CloseInactiveIssuesUseCase', () => { + const listOpenIssuesByLabel = jest.fn(); + const getOpenIssue = jest.fn(); + const closeIssue = jest.fn(); + const addComment = jest.fn(); + const nowMilliseconds = Date.parse('2026-09-04T00:00:00.000Z'); + + function createUseCase() { + return new CloseInactiveIssuesUseCase( + { listOpenIssuesByLabel, getOpenIssue }, + { closeIssue, addComment }, + { nowMilliseconds: () => nowMilliseconds }, + ); + } + + beforeEach(() => { + jest.clearAllMocks(); + listOpenIssuesByLabel.mockResolvedValue([]); + getOpenIssue.mockImplementation(async (_owner: string, _repo: string, issueNumber: number) => snapshot({ number: issueNumber })); + closeIssue.mockResolvedValue(true); + addComment.mockResolvedValue(undefined); + }); + + it('scans both waiting queues, revalidates, closes, and comments stale issues', async () => { + listOpenIssuesByLabel + .mockResolvedValueOnce([snapshot()]) + .mockResolvedValueOnce([snapshot({ number: 42, labels: ['state:awaiting-issue-author'] })]); + + const [result] = await createUseCase().invoke(execution()); + + expect(listOpenIssuesByLabel).toHaveBeenCalledTimes(2); + expect(getOpenIssue).toHaveBeenCalledWith('owner', 'repo', 42, 'token'); + expect(closeIssue).toHaveBeenCalledWith('owner', 'repo', 42, 'token'); + expect(addComment).toHaveBeenCalledWith( + 'owner', + 'repo', + 42, + expect.stringContaining('automatically closed due to inactivity'), + 'token', + ); + expect(result).toMatchObject({ + id: 'CloseInactiveIssuesUseCase', + success: true, + executed: true, + payload: { scanned: 1, eligible: 1, closed: 1, skipped: 0 }, + }); + }); + + it('does not close pull requests or recently active issues', async () => { + listOpenIssuesByLabel + .mockResolvedValueOnce([snapshot({ isPullRequest: true })]) + .mockResolvedValueOnce([snapshot({ number: 43, updatedAt: '2026-09-03T00:00:01.000Z' })]); + + const [result] = await createUseCase().invoke(execution()); + + expect(closeIssue).not.toHaveBeenCalled(); + expect(addComment).not.toHaveBeenCalled(); + expect(result.payload).toEqual({ scanned: 2, eligible: 0, closed: 0, skipped: 2 }); + }); + + it('skips a candidate that becomes active before the mutation', async () => { + listOpenIssuesByLabel.mockResolvedValueOnce([snapshot()]); + getOpenIssue.mockResolvedValue(snapshot({ updatedAt: '2026-09-03T00:00:01.000Z' })); + + const [result] = await createUseCase().invoke(execution()); + + expect(closeIssue).not.toHaveBeenCalled(); + expect(result.payload).toEqual({ scanned: 1, eligible: 1, closed: 0, skipped: 1 }); + }); + + it('continues scanning when one candidate mutation fails and reports failure', async () => { + listOpenIssuesByLabel.mockResolvedValueOnce([snapshot(), snapshot({ number: 43 })]); + closeIssue.mockRejectedValueOnce(new Error('provider unavailable')).mockResolvedValueOnce(true); + + const [result] = await createUseCase().invoke(execution()); + + expect(closeIssue).toHaveBeenCalledTimes(2); + expect(result.success).toBe(false); + expect(result.errors[0].message).toContain('Unable to close issue #42'); + expect(addComment).toHaveBeenCalledTimes(1); + }); + + it('does not comment when the idempotent close operation reports that the issue is already closed', async () => { + listOpenIssuesByLabel.mockResolvedValueOnce([snapshot()]); + closeIssue.mockResolvedValue(false); + + const [result] = await createUseCase().invoke(execution()); + + expect(addComment).not.toHaveBeenCalled(); + expect(result).toMatchObject({ + success: true, + payload: { scanned: 1, eligible: 1, closed: 0, skipped: 1 }, + }); + }); + + it('returns a failure when the candidate scan cannot be completed', async () => { + listOpenIssuesByLabel.mockRejectedValue(new Error('rate limited')); + + const [result] = await createUseCase().invoke(execution()); + + expect(result.success).toBe(false); + expect(result.steps[0]).toContain('Unable to scan issues'); + expect(closeIssue).not.toHaveBeenCalled(); + }); +}); diff --git a/src/application/usecases/actions/__tests__/create_release_use_case.test.ts b/src/application/usecases/actions/__tests__/create_release_use_case.test.ts index 6ce0d9dd..794276dc 100644 --- a/src/application/usecases/actions/__tests__/create_release_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/create_release_use_case.test.ts @@ -1,6 +1,6 @@ import { CreateReleaseUseCase } from '../create_release_use_case'; import { Result } from '../../../../data/model/result'; -import { INPUT_KEYS } from '../../../../utils/constants'; +import { INPUT_KEYS } from '../../../contracts/input_keys'; import type { Execution } from '../../../../data/model/execution'; jest.mock('../../../../utils/logger', () => ({ diff --git a/src/application/usecases/actions/__tests__/create_tag_use_case.test.ts b/src/application/usecases/actions/__tests__/create_tag_use_case.test.ts index 40cc803a..1cc020b7 100644 --- a/src/application/usecases/actions/__tests__/create_tag_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/create_tag_use_case.test.ts @@ -1,6 +1,6 @@ import { CreateTagUseCase } from '../create_tag_use_case'; import { Result } from '../../../../data/model/result'; -import { INPUT_KEYS } from '../../../../utils/constants'; +import { INPUT_KEYS } from '../../../contracts/input_keys'; import type { Execution } from '../../../../data/model/execution'; jest.mock('../../../../utils/logger', () => ({ diff --git a/src/application/usecases/actions/__tests__/initial_setup_request.test.ts b/src/application/usecases/actions/__tests__/initial_setup_request.test.ts new file mode 100644 index 00000000..f74a1e63 --- /dev/null +++ b/src/application/usecases/actions/__tests__/initial_setup_request.test.ts @@ -0,0 +1,53 @@ +import type { Execution } from '../../../../data/model/execution'; +import { createDefaultSetupConfiguration } from '../../../policies/setup_configuration_policy'; +import { createInitialSetupRequest } from '../initial_setup_request'; + +describe('createInitialSetupRequest', () => { + it('maps only setup facts from the legacy execution aggregate', () => { + const setupConfiguration = createDefaultSetupConfiguration(); + const execution = { + owner: 'owner', + repo: 'repo', + tokens: { token: 'token' }, + labels: { configured: true }, + issueTypes: { bug: true }, + inputs: { + setupConfiguration, + setupCredentials: { workflowPat: { name: 'PAT', value: 'secret' }, apiKeys: [] }, + setupRemoteConfiguration: { ownerType: 'Organization' }, + setupWorkflowUpdates: ['a.yml', 42, 'b.yml'], + unrelatedExecutionState: { shouldNotBeCopied: true }, + }, + } as unknown as Execution; + + expect(createInitialSetupRequest(execution)).toEqual({ + owner: 'owner', + repo: 'repo', + token: 'token', + labels: execution.labels, + issueTypes: execution.issueTypes, + setupConfiguration, + setupCredentials: execution.inputs?.setupCredentials, + setupRemoteConfiguration: execution.inputs?.setupRemoteConfiguration, + workflowUpdates: ['a.yml', 'b.yml'], + }); + }); + + it('uses safe empty setup input defaults', () => { + const execution = { + owner: 'owner', + repo: 'repo', + tokens: { token: 'token' }, + labels: {}, + issueTypes: {}, + inputs: { setupWorkflowUpdates: 'not-an-array' }, + } as unknown as Execution; + + expect(createInitialSetupRequest(execution)).toMatchObject({ + setupConfiguration: undefined, + setupCredentials: undefined, + setupRemoteConfiguration: undefined, + workflowUpdates: [], + }); + }); +}); diff --git a/src/application/usecases/actions/__tests__/publish_github_action_use_case.test.ts b/src/application/usecases/actions/__tests__/publish_github_action_use_case.test.ts index b123de82..db2f6f64 100644 --- a/src/application/usecases/actions/__tests__/publish_github_action_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/publish_github_action_use_case.test.ts @@ -1,6 +1,6 @@ import { PublishGithubActionUseCase } from '../publish_github_action_use_case'; import { Result } from '../../../../data/model/result'; -import { INPUT_KEYS } from '../../../../utils/constants'; +import { INPUT_KEYS } from '../../../contracts/input_keys'; import type { Execution } from '../../../../data/model/execution'; jest.mock('../../../../utils/logger', () => ({ diff --git a/src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts b/src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts new file mode 100644 index 00000000..677e04dc --- /dev/null +++ b/src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts @@ -0,0 +1,159 @@ +import { createDefaultSetupConfiguration } from '../../../policies/setup_configuration_policy'; +import { + ensureRepositorySecrets, + ensureRepositoryVariables, + groupSetupResources, + resolveRemoteConfiguration, +} from '../setup_resource_provisioning'; + +const context = { + owner: 'owner', + repo: 'repo', + token: 'token', +}; + +describe('setup resource provisioning policy', () => { + it('keeps an effective inherited variable instead of shadowing it', () => { + const configuration = createDefaultSetupConfiguration(); + configuration.storage.variables.defaultScope = 'organization'; + const groups = groupSetupResources([ + { name: 'AGENT_PROVIDER', value: 'codex' }, + { name: 'AGENT_MODEL', value: 'gpt-5.6' }, + ], 'variable', configuration, { + ownerType: 'Organization', + repositoryId: 42, + repositoryVisibility: 'private', + repositorySecrets: [], + organizationSecrets: [], + repositoryVariables: [{ name: 'AGENT_MODEL', value: 'inherited' }], + organizationVariables: [], + organizationAccess: 'available', + organizationSecretsAccess: 'available', + organizationVariablesAccess: 'available', + }); + + expect(groups).toHaveLength(1); + expect(groups[0].target.scope).toBe('organization'); + expect(groups[0].resources).toEqual([{ name: 'AGENT_PROVIDER', value: 'codex' }]); + }); + + it('groups resources by explicit scope and preserves target metadata', () => { + const configuration = createDefaultSetupConfiguration(); + configuration.storage.secrets.defaultScope = 'organization'; + configuration.storage.secrets.overrides = { PAT: 'repository' }; + + const groups = groupSetupResources([ + { name: 'PAT', value: 'workflow-token' }, + { name: 'OPENAI_API_KEY', value: 'api-key' }, + ], 'secret', configuration, { + ownerType: 'Organization', + repositoryId: 7, + repositoryVisibility: 'private', + repositorySecrets: [], + organizationSecrets: [], + repositoryVariables: [], + organizationVariables: [], + organizationAccess: 'available', + organizationSecretsAccess: 'available', + organizationVariablesAccess: 'available', + }); + + expect(groups).toEqual([ + { + target: { scope: 'repository', organizationVisibility: 'selected', repositoryId: 7 }, + resources: [{ name: 'PAT', value: 'workflow-token' }], + }, + { + target: { scope: 'organization', organizationVisibility: 'selected', repositoryId: 7 }, + resources: [{ name: 'OPENAI_API_KEY', value: 'api-key' }], + }, + ]); + }); + + it('provisions validated credentials through the repository secret port', async () => { + const upsertSecrets = jest.fn().mockResolvedValue({ created: 1, updated: 1, skipped: 0, errors: [] }); + const configuration = createDefaultSetupConfiguration(); + + const result = await ensureRepositorySecrets( + { + ...context, + setupCredentials: { + workflowPat: { name: 'PAT', value: 'workflow-token' }, + apiKeys: [{ name: 'OPENAI_API_KEY', value: 'api-key' }], + }, + }, + { setupRepositorySecretsPort: { list: jest.fn(), upsertSecrets } }, + configuration, + ); + + expect(result.errors).toEqual([]); + expect(result.step).toContain('1 created, 1 updated'); + expect(upsertSecrets).toHaveBeenCalledWith('owner', 'repo', 'token', [ + { name: 'PAT', value: 'workflow-token' }, + { name: 'OPENAI_API_KEY', value: 'api-key' }, + ]); + }); + + it('reports when setup secrets are enabled without validated credentials', async () => { + const result = await ensureRepositorySecrets( + context, + { setupRepositorySecretsPort: { list: jest.fn(), upsertSecrets: jest.fn() } }, + createDefaultSetupConfiguration(), + ); + + expect(result.errors).toEqual([]); + expect(result.step).toContain('were not changed'); + }); + + it('uses the organization variable port when the resolved target is organizational', async () => { + const upsertScopedVariables = jest.fn().mockResolvedValue({ created: 2, updated: 0, errors: [] }); + const configuration = createDefaultSetupConfiguration(); + configuration.storage.variables.defaultScope = 'organization'; + + const result = await ensureRepositoryVariables( + context, + { setupRepositoryVariablesPort: { upsert: jest.fn(), upsertScopedVariables } }, + configuration, + { + ownerType: 'Organization', + repositoryId: 42, + repositoryVisibility: 'private', + repositorySecrets: [], + organizationSecrets: [], + repositoryVariables: [], + organizationVariables: [], + organizationAccess: 'available', + organizationSecretsAccess: 'available', + organizationVariablesAccess: 'available', + }, + ); + + expect(result.errors).toEqual([]); + expect(result.step).toContain('2 created, 0 updated'); + expect(upsertScopedVariables).toHaveBeenCalled(); + }); + + it('returns a provided remote snapshot without calling the read port', async () => { + const provided = { + ownerType: 'User' as const, + repositoryVisibility: 'public' as const, + repositorySecrets: [], + organizationSecrets: [], + repositoryVariables: [], + organizationVariables: [], + organizationAccess: 'not_applicable' as const, + organizationSecretsAccess: 'not_applicable' as const, + organizationVariablesAccess: 'not_applicable' as const, + }; + const inspect = jest.fn(); + + await expect(resolveRemoteConfiguration( + { ...context, setupRemoteConfiguration: provided }, + { setupRemoteConfigurationReadPort: { inspect } }, + createDefaultSetupConfiguration(), + [], + )).resolves.toBe(provided); + + expect(inspect).not.toHaveBeenCalled(); + }); +}); diff --git a/src/application/usecases/actions/close_inactive_issues_use_case.ts b/src/application/usecases/actions/close_inactive_issues_use_case.ts new file mode 100644 index 00000000..54db872c --- /dev/null +++ b/src/application/usecases/actions/close_inactive_issues_use_case.ts @@ -0,0 +1,25 @@ +import type { Execution } from '../../../data/model/execution'; +import type { Result } from '../../../data/model/result'; +import { ParamUseCase } from '../base/param_usecase'; +import type { IssueClosurePort } from '../../ports/issue_lifecycle_ports'; +import type { IssueInactivityClockPort, IssueInactivityQueryPort } from '../../ports/issue_inactivity_ports'; +import { runCloseInactiveIssuesWorkflow } from './close_inactive_issues_workflow'; + +/** Application boundary for the scheduled inactivity-maintenance action. */ +export class CloseInactiveIssuesUseCase implements ParamUseCase { + taskId = 'CloseInactiveIssuesUseCase'; + + constructor( + private readonly issueQueryPort: IssueInactivityQueryPort, + private readonly issueClosurePort: IssueClosurePort, + private readonly clock: IssueInactivityClockPort, + ) {} + + async invoke(param: Execution): Promise { + return runCloseInactiveIssuesWorkflow(param, { + issueQueryPort: this.issueQueryPort, + issueClosurePort: this.issueClosurePort, + clock: this.clock, + }); + } +} diff --git a/src/application/usecases/actions/close_inactive_issues_workflow.ts b/src/application/usecases/actions/close_inactive_issues_workflow.ts new file mode 100644 index 00000000..02763e27 --- /dev/null +++ b/src/application/usecases/actions/close_inactive_issues_workflow.ts @@ -0,0 +1,164 @@ +import type { Execution } from '../../../data/model/execution'; +import { Result } from '../../../data/model/result'; +import { evaluateIssueInactivity, type IssueActivitySnapshot } from '../../../domain/issue_inactivity'; +import type { IssueClosurePort } from '../../ports/issue_lifecycle_ports'; +import type { IssueInactivityClockPort, IssueInactivityQueryPort } from '../../ports/issue_inactivity_ports'; +import { sanitizePublishedError } from '../../policies/github_comment_publication_policy'; +import { logDebugInfo, logError, logInfo } from '../../ports/logging_ports'; + +export interface CloseInactiveIssuesWorkflowDependencies { + readonly issueQueryPort: IssueInactivityQueryPort; + readonly issueClosurePort: IssueClosurePort; + readonly clock: IssueInactivityClockPort; +} + +const TASK_ID = 'CloseInactiveIssuesUseCase'; +const INACTIVITY_COMMENT = (thresholdHours: number): string => + `This issue was automatically closed due to inactivity while waiting for a response. No activity was detected for at least **${thresholdHours} hours**. Reopen it and add a comment if it still needs attention.`; + +/** Scans waiting issues and closes only candidates that remain inactive. */ +export async function runCloseInactiveIssuesWorkflow( + param: Execution, + dependencies: CloseInactiveIssuesWorkflowDependencies, +): Promise { + const waitingLabels = unique([ + param.labels.lifecycle.awaitingMaintainer, + param.labels.lifecycle.awaitingIssueAuthor, + ]); + const activityLabel = param.labels.lifecycle.aiProcessing; + const nowMilliseconds = dependencies.clock.nowMilliseconds(); + const thresholdHours = param.inactivityThresholdHours; + + try { + const candidates = await listCandidates(param, waitingLabels, dependencies.issueQueryPort); + let eligibleCount = 0; + let closedCount = 0; + let skippedCount = 0; + const errors: string[] = []; + + for (const candidate of candidates) { + const initialDecision = evaluateIssueInactivity({ + issue: candidate, + waitingLabels, + agentActivityLabel: activityLabel, + thresholdHours, + nowMilliseconds, + }); + if (initialDecision.kind !== 'close') { + skippedCount++; + continue; + } + eligibleCount++; + + try { + // Re-read both labels and updated_at immediately before the + // mutation so a comment or state transition during the scan + // invalidates the stale list snapshot. + const current = await dependencies.issueQueryPort.getOpenIssue( + param.owner, + param.repo, + candidate.number, + param.tokens.token, + ); + if (!current || evaluateIssueInactivity({ + issue: current, + waitingLabels, + agentActivityLabel: activityLabel, + thresholdHours, + nowMilliseconds: dependencies.clock.nowMilliseconds(), + }).kind !== 'close') { + skippedCount++; + continue; + } + + const closed = await dependencies.issueClosurePort.closeIssue( + param.owner, + param.repo, + candidate.number, + param.tokens.token, + ); + if (!closed) { + skippedCount++; + continue; + } + closedCount++; + await dependencies.issueClosurePort.addComment( + param.owner, + param.repo, + candidate.number, + INACTIVITY_COMMENT(thresholdHours), + param.tokens.token, + ); + logInfo(`Issue #${candidate.number} closed after inactivity.`); + } catch (error) { + const message = `Unable to close issue #${candidate.number} after inactivity.`; + logError(message); + errors.push(`${message} ${safeErrorMessage(error)}`); + } + } + + logDebugInfo( + `${TASK_ID}: scanned=${candidates.length}, eligible=${eligibleCount}, closed=${closedCount}, skipped=${skippedCount}.`, + ); + return [new Result({ + id: TASK_ID, + success: errors.length === 0, + executed: closedCount > 0 || eligibleCount > 0, + steps: buildSteps(candidates.length, closedCount, skippedCount), + payload: { + scanned: candidates.length, + eligible: eligibleCount, + closed: closedCount, + skipped: skippedCount, + }, + errors, + })]; + } catch (error) { + const message = 'Unable to scan issues for inactivity closure.'; + logError(message); + return [new Result({ + id: TASK_ID, + success: false, + executed: true, + steps: [message], + errors: [`${message} ${safeErrorMessage(error)}`], + })]; + } +} + +async function listCandidates( + param: Execution, + waitingLabels: readonly string[], + queryPort: IssueInactivityQueryPort, +): Promise { + const candidates: IssueActivitySnapshot[] = []; + for (const label of waitingLabels) { + candidates.push(...await queryPort.listOpenIssuesByLabel( + param.owner, + param.repo, + label, + param.tokens.token, + )); + } + + const uniqueCandidates = new Map(); + for (const candidate of candidates) uniqueCandidates.set(candidate.number, candidate); + return [...uniqueCandidates.values()]; +} + +function buildSteps(scanned: number, closed: number, skipped: number): string[] { + const steps = [`Scanned ${scanned} open issue(s) waiting for a response.`]; + if (closed > 0) steps.push(`Closed ${closed} issue(s) after the inactivity threshold.`); + if (skipped > 0) steps.push(`Skipped ${skipped} candidate(s) because they were no longer eligible.`); + if (closed === 0) steps.push('No issue was closed for inactivity.'); + return steps; +} + +function unique(values: readonly string[]): string[] { + return [...new Set(values.map(value => value.trim()).filter(Boolean))]; +} + +function safeErrorMessage(error: unknown): string { + const message = sanitizePublishedError(error instanceof Error ? error.message : error); + return message || 'Unknown provider error.'; +} diff --git a/src/application/usecases/actions/create_release_policy.ts b/src/application/usecases/actions/create_release_policy.ts index 60f64732..8f6cd88a 100644 --- a/src/application/usecases/actions/create_release_policy.ts +++ b/src/application/usecases/actions/create_release_policy.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../../../utils/constants'; +import { INPUT_KEYS } from '../../contracts/input_keys'; import { ApplicationError } from '../../errors/application_error'; const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/; diff --git a/src/application/usecases/actions/create_tag_workflow.ts b/src/application/usecases/actions/create_tag_workflow.ts index fcdaaefc..1b521097 100644 --- a/src/application/usecases/actions/create_tag_workflow.ts +++ b/src/application/usecases/actions/create_tag_workflow.ts @@ -1,7 +1,7 @@ import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; import type { RepositoryTagPort } from '../../ports/repository_release_ports'; -import { INPUT_KEYS } from '../../../utils/constants'; +import { INPUT_KEYS } from '../../contracts/input_keys'; import { logError, logWarn } from '../../ports/logging_ports'; export async function runCreateTag( diff --git a/src/application/usecases/actions/initial_setup_request.ts b/src/application/usecases/actions/initial_setup_request.ts new file mode 100644 index 00000000..a3135730 --- /dev/null +++ b/src/application/usecases/actions/initial_setup_request.ts @@ -0,0 +1,40 @@ +import type { Execution } from '../../../data/model/execution'; +import type { IssueTypes } from '../../../data/model/issue_types'; +import type { Labels } from '../../../data/model/labels'; +import type { + SetupConfiguration, + SetupCredentialCollection, + SetupRemoteConfiguration, +} from '../../../domain/setup'; +import type { SetupRepositoryContext } from './setup_resource_provisioning'; + +/** Narrow input assembled by the execution adapter for the setup workflow. */ +export interface InitialSetupRequest extends SetupRepositoryContext { + labels: Labels; + issueTypes: IssueTypes; + setupConfiguration?: SetupConfiguration; + workflowUpdates: readonly string[]; +} + +/** Converts the legacy execution aggregate into the setup use case's explicit request. */ +export function createInitialSetupRequest(execution: Execution): InitialSetupRequest { + return { + owner: execution.owner, + repo: execution.repo, + token: execution.tokens.token, + labels: execution.labels, + issueTypes: execution.issueTypes, + setupConfiguration: asObject(execution.inputs?.setupConfiguration), + setupCredentials: asObject(execution.inputs?.setupCredentials), + setupRemoteConfiguration: asObject(execution.inputs?.setupRemoteConfiguration), + workflowUpdates: asStringArray(execution.inputs?.setupWorkflowUpdates), + }; +} + +function asObject(value: unknown): T | undefined { + return value && typeof value === 'object' ? value as T : undefined; +} + +function asStringArray(value: unknown): string[] { + return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []; +} diff --git a/src/application/usecases/actions/initial_setup_use_case.ts b/src/application/usecases/actions/initial_setup_use_case.ts index 497d2fed..5cf50243 100644 --- a/src/application/usecases/actions/initial_setup_use_case.ts +++ b/src/application/usecases/actions/initial_setup_use_case.ts @@ -7,6 +7,7 @@ import type { InitialLabelProvisioningPort, IssueTypeProvisioningPort } from '.. import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { ParamUseCase } from '../base/param_usecase'; import { runInitialSetupWorkflow } from './initial_setup_workflow'; +import { createInitialSetupRequest } from './initial_setup_request'; import type { SetupRemoteConfigurationReadPort, SetupRepositorySecretsPort, @@ -31,7 +32,7 @@ export class InitialSetupUseCase implements ParamUseCase { ) {} async invoke(param: Execution): Promise { - return await runInitialSetupWorkflow(param, { + return await runInitialSetupWorkflow(createInitialSetupRequest(param), { authenticatedUserPort: this.authenticatedUserPort, initialLabelProvisioningPort: this.initialLabelProvisioningPort, issueTypeProvisioningPort: this.issueTypeProvisioningPort, diff --git a/src/application/usecases/actions/initial_setup_workflow.ts b/src/application/usecases/actions/initial_setup_workflow.ts index df293261..551365fc 100644 --- a/src/application/usecases/actions/initial_setup_workflow.ts +++ b/src/application/usecases/actions/initial_setup_workflow.ts @@ -1,4 +1,3 @@ -import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; import type { LatestTagQueryPort } from '../../ports/branch_tag_ports'; import type { AuthenticatedUserPort } from '../../ports/authenticated_user_ports'; @@ -12,20 +11,16 @@ import type { SetupWorkspacePort } from '../../ports/setup_workspace_ports'; import { DEFAULT_INITIAL_TAG } from '../../../data/model/version_policy'; import { logDebugInfo, logError, logInfo } from '../../ports/logging_ports'; import { getTaskEmoji } from '../../../utils/task_emoji'; -import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration, SetupResourceTarget } from '../../../domain/setup'; -import type { - SetupRemoteConfigurationReadPort, - SetupRepositorySecretsPort, - SetupRepositoryVariablesPort, -} from '../../ports/setup_wizard_ports'; +import type { SetupConfiguration } from '../../../domain/setup'; +import type { SetupResourceProvisioningDependencies } from './setup_resource_provisioning'; +import type { InitialSetupRequest } from './initial_setup_request'; import { - buildSetupRepositoryVariables, - resolveSetupResourceTarget, - shouldUpsertSetupResource, - usesOrganizationStorage, -} from '../../policies/setup_configuration_policy'; + ensureRepositorySecrets, + ensureRepositoryVariables, + resolveRemoteConfiguration, +} from './setup_resource_provisioning'; -export interface InitialSetupWorkflowDependencies { +export interface InitialSetupWorkflowDependencies extends SetupResourceProvisioningDependencies { authenticatedUserPort: AuthenticatedUserPort; initialLabelProvisioningPort: InitialLabelProvisioningPort; issueTypeProvisioningPort: IssueTypeProvisioningPort; @@ -33,9 +28,6 @@ export interface InitialSetupWorkflowDependencies { repositoryDefaultBranchPort: RepositoryDefaultBranchPort; repositoryTagPort: RepositoryTagPort; setupWorkspacePort: SetupWorkspacePort; - setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; - setupRepositorySecretsPort?: SetupRepositorySecretsPort; - setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; } type InitialLabelProvisioningOutcome = @@ -46,7 +38,7 @@ const TASK_ID = 'InitialSetupUseCase'; /** Runs repository setup as an ordered application workflow with explicit port dependencies. */ export async function runInitialSetupWorkflow( - param: Execution, + request: InitialSetupRequest, dependencies: InitialSetupWorkflowDependencies, ): Promise { logInfo(`${getTaskEmoji(TASK_ID)} Executing ${TASK_ID}.`); @@ -54,39 +46,38 @@ export async function runInitialSetupWorkflow( const errors: string[] = []; try { - const setupConfiguration = getSetupConfiguration(param); - if (!dependencies.setupWorkspacePort.hasValidToken(param.tokens.token)) { + const setupConfiguration = request.setupConfiguration; + if (!dependencies.setupWorkspacePort.hasValidToken(request.token)) { logInfo(' 🛑 Setup requires the setup PAT provided for this command with a valid token.'); errors.push('A valid setup PAT must be provided to run setup. It is separate from the workflow PAT Secret.'); return [buildResult(errors, steps)]; } logInfo('📋 Ensuring .github and copying setup files...'); - const workflowUpdates = getWorkflowUpdates(param); const workspaceSelection = { features: setupConfiguration?.features, - ...(workflowUpdates.length > 0 ? { + ...(request.workflowUpdates.length > 0 ? { updateExistingWorkflows: true, - approvedWorkflowFiles: workflowUpdates, + approvedWorkflowFiles: request.workflowUpdates, } : {}), }; const filesResult = dependencies.setupWorkspacePort.prepare(workspaceSelection); steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`); logInfo('🔐 Checking GitHub access...'); - const githubAccess = await verifyGitHubAccess(param, dependencies.authenticatedUserPort); + const githubAccess = await verifyGitHubAccess(request, dependencies.authenticatedUserPort); if (!githubAccess.success) { errors.push(...githubAccess.errors); return [buildResult(errors, steps)]; } steps.push(`✅ GitHub access verified: ${githubAccess.user}`); - const remoteConfiguration = await resolveRemoteConfiguration(param, dependencies, setupConfiguration, errors); + const remoteConfiguration = await resolveRemoteConfiguration(request, dependencies, setupConfiguration, errors); - const secrets = await ensureRepositorySecrets(param, dependencies, setupConfiguration, remoteConfiguration); + const secrets = await ensureRepositorySecrets(request, dependencies, setupConfiguration, remoteConfiguration); if (secrets.step) steps.push(secrets.step); if (secrets.errors.length > 0) errors.push(...secrets.errors); logInfo('🏷️ Checking configured and progress labels...'); - const labels = await ensureInitialLabels(param, dependencies.initialLabelProvisioningPort); + const labels = await ensureInitialLabels(request, dependencies.initialLabelProvisioningPort); if (!labels.completed) { errors.push(labels.error); } else { @@ -95,18 +86,18 @@ export async function runInitialSetupWorkflow( } logInfo('📋 Checking issue types...'); - const issueTypes = await ensureIssueTypes(param, dependencies.issueTypeProvisioningPort); + const issueTypes = await ensureIssueTypes(request, dependencies.issueTypeProvisioningPort); if (!issueTypes.success) { errors.push(...issueTypes.errors); } else { steps.push(`✅ Issue types checked: ${issueTypes.created} created, ${issueTypes.existing} already existed`); } - const variables = await ensureRepositoryVariables(param, dependencies, setupConfiguration, remoteConfiguration); + const variables = await ensureRepositoryVariables(request, dependencies, setupConfiguration, remoteConfiguration); if (variables.step) steps.push(variables.step); if (variables.errors.length > 0) errors.push(...variables.errors); - const defaultVersion = await ensureDefaultVersion(param, dependencies, setupConfiguration); + const defaultVersion = await ensureDefaultVersion(request, dependencies, setupConfiguration); if (defaultVersion.step) steps.push(defaultVersion.step); if (defaultVersion.error) errors.push(defaultVersion.error); return [buildResult(errors, steps)]; @@ -118,11 +109,11 @@ export async function runInitialSetupWorkflow( } async function verifyGitHubAccess( - param: Execution, + request: InitialSetupRequest, repository: AuthenticatedUserPort, ): Promise<{ success: boolean; user?: string; errors: string[] }> { try { - const user = await repository.getUserFromToken(param.tokens.token); + const user = await repository.getUserFromToken(request.token); return { success: true, user, errors: [] }; } catch (error) { logError(`Error verifying GitHub access: ${error}`); @@ -131,15 +122,15 @@ async function verifyGitHubAccess( } async function ensureInitialLabels( - param: Execution, + request: InitialSetupRequest, repository: InitialLabelProvisioningPort, ): Promise { try { const summary = await repository.ensureInitialLabels( - param.owner, - param.repo, - param.labels, - param.tokens.token, + request.owner, + request.repo, + request.labels, + request.token, ); return { completed: true, ...summary }; } catch (error) { @@ -150,14 +141,14 @@ async function ensureInitialLabels( } async function ensureIssueTypes( - param: Execution, + request: InitialSetupRequest, repository: IssueTypeProvisioningPort, ): Promise<{ success: boolean; created: number; existing: number; errors: string[] }> { try { const result = await repository.ensureIssueTypes( - param.owner, - param.issueTypes, - param.tokens.token, + request.owner, + request.issueTypes, + request.token, ); return { success: result.errors.length === 0, @@ -172,7 +163,7 @@ async function ensureIssueTypes( } async function ensureDefaultVersion( - param: Execution, + request: InitialSetupRequest, dependencies: InitialSetupWorkflowDependencies, setupConfiguration?: SetupConfiguration, ): Promise<{ step?: string; error?: string }> { @@ -188,9 +179,9 @@ async function ensureDefaultVersion( logInfo(`🏷️ No version tags found. Creating default tag ${DEFAULT_INITIAL_TAG}...`); const defaultBranch = await dependencies.repositoryDefaultBranchPort.getDefaultBranch( - param.owner, - param.repo, - param.tokens.token, + request.owner, + request.repo, + request.token, ); if (!defaultBranch) { const message = 'Could not get default branch to create initial version tag.'; @@ -199,15 +190,15 @@ async function ensureDefaultVersion( } const sha = await dependencies.repositoryTagPort.createTag( - param.owner, - param.repo, + request.owner, + request.repo, defaultBranch, DEFAULT_INITIAL_TAG, - param.tokens.token, + request.token, ); return sha ? { step: `✅ Default version tag ${DEFAULT_INITIAL_TAG} created on branch ${defaultBranch}. Run \`git fetch --tags\` to update local refs.` } - : { error: `Failed to create tag ${DEFAULT_INITIAL_TAG} on ${param.owner}/${param.repo}` }; + : { error: `Failed to create tag ${DEFAULT_INITIAL_TAG} on ${request.owner}/${request.repo}` }; } catch (error) { const message = `Error ensuring default version: ${error}`; logError(message); @@ -215,173 +206,6 @@ async function ensureDefaultVersion( } } -function getSetupConfiguration(param: Execution): SetupConfiguration | undefined { - const configuration = param.inputs?.setupConfiguration; - return configuration && typeof configuration === 'object' - ? configuration as SetupConfiguration - : undefined; -} - -function getWorkflowUpdates(param: Execution): string[] { - const updates = param.inputs?.setupWorkflowUpdates; - return Array.isArray(updates) ? updates.filter((file): file is string => typeof file === 'string') : []; -} - -async function ensureRepositoryVariables( - param: Execution, - dependencies: InitialSetupWorkflowDependencies, - setupConfiguration?: SetupConfiguration, - remoteConfiguration?: SetupRemoteConfiguration, -): Promise<{ step?: string; errors: string[] }> { - if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { - return { errors: [] }; - } - try { - const desired = buildSetupRepositoryVariables(setupConfiguration); - const groups = groupResources(desired, 'variable', setupConfiguration, remoteConfiguration); - const result = await upsertVariableGroups(param, dependencies.setupRepositoryVariablesPort, groups); - if (result.errors.length > 0) return { errors: result.errors }; - return { - step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, - errors: [], - }; - } catch (error) { - const message = `Error configuring repository Variables: ${error}`; - logError(message); - return { errors: [message] }; - } -} - -async function ensureRepositorySecrets( - param: Execution, - dependencies: InitialSetupWorkflowDependencies, - setupConfiguration?: SetupConfiguration, - remoteConfiguration?: SetupRemoteConfiguration, -): Promise<{ step?: string; errors: string[] }> { - if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { - return { errors: [] }; - } - const credentials = getSetupCredentialCollection(param); - if (!credentials) { - return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; - } - const values = [ - ...(credentials.workflowPat ? [credentials.workflowPat] : []), - ...credentials.apiKeys, - ]; - if (values.length === 0) return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; - try { - const groups = groupResources(values, 'secret', setupConfiguration, remoteConfiguration); - const result = await upsertSecretGroups(param, dependencies.setupRepositorySecretsPort, groups); - if (result.errors.length > 0) return { errors: result.errors }; - return { - step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, - errors: [], - }; - } catch (error) { - const message = `Error configuring repository Secrets: ${error}`; - logError(message); - return { errors: [message] }; - } -} - -async function resolveRemoteConfiguration( - param: Execution, - dependencies: InitialSetupWorkflowDependencies, - setupConfiguration: SetupConfiguration | undefined, - errors: string[], -): Promise { - const provided = param.inputs?.setupRemoteConfiguration; - if (provided && typeof provided === 'object') return provided as SetupRemoteConfiguration; - if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) return undefined; - try { - return await dependencies.setupRemoteConfigurationReadPort.inspect(param.owner, param.repo, param.tokens.token); - } catch (error) { - const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; - logError(message); - if (usesOrganizationStorage(setupConfiguration)) errors.push(message); - return undefined; - } -} - -type SetupResource = { name: string; value: string }; -type ResourceGroup = { target: SetupResourceTarget; resources: SetupResource[] }; - -function groupResources( - resources: readonly SetupResource[], - kind: 'secret' | 'variable', - configuration: SetupConfiguration, - remoteConfiguration?: SetupRemoteConfiguration, -): ResourceGroup[] { - const groups = new Map(); - for (const resource of resources) { - // Secret values reach this workflow only after the user chose keep/replace. - // Variables, however, are always generated from the selected setup contract, - // so preserveExisting must be applied here to avoid shadowing inherited values. - if (kind === 'variable' && !shouldUpsertSetupResource(configuration, kind, resource.name, remoteConfiguration)) continue; - const target = resolveSetupResourceTarget(configuration, kind, resource.name, remoteConfiguration); - const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; - const group = groups.get(key) ?? { target, resources: [] }; - group.resources.push(resource); - groups.set(key, group); - } - return [...groups.values()]; -} - -async function upsertVariableGroups( - param: Execution, - port: SetupRepositoryVariablesPort, - groups: readonly ResourceGroup[], -): Promise<{ created: number; updated: number; errors: string[] }> { - let created = 0; - let updated = 0; - const errors: string[] = []; - for (const group of groups) { - if (group.target.scope === 'organization' && !port.upsertScopedVariables) { - errors.push('Organization Variable provisioning is not available in this installation.'); - continue; - } - const result = group.target.scope === 'organization' - ? await port.upsertScopedVariables!(param.owner, param.repo, param.tokens.token, group.target, group.resources) - : await port.upsert(param.owner, param.repo, param.tokens.token, group.resources); - created += result.created; - updated += result.updated; - errors.push(...result.errors); - } - return { created, updated, errors }; -} - -async function upsertSecretGroups( - param: Execution, - port: SetupRepositorySecretsPort, - groups: readonly ResourceGroup[], -): Promise<{ created: number; updated: number; skipped: number; errors: string[] }> { - let created = 0; - let updated = 0; - let skipped = 0; - const errors: string[] = []; - for (const group of groups) { - if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { - errors.push('Organization Secret provisioning is not available in this installation.'); - continue; - } - const result = group.target.scope === 'organization' - ? await port.upsertScopedSecrets!(param.owner, param.repo, param.tokens.token, group.target, group.resources) - : await port.upsertSecrets(param.owner, param.repo, param.tokens.token, group.resources); - created += result.created; - updated += result.updated; - skipped += result.skipped; - errors.push(...result.errors); - } - return { created, updated, skipped, errors }; -} - -function getSetupCredentialCollection(param: Execution): SetupCredentialCollection | undefined { - const credentials = param.inputs?.setupCredentials; - if (!credentials || typeof credentials !== 'object') return undefined; - return credentials as SetupCredentialCollection; -} - function appendLabelSummary( steps: string[], errors: string[], diff --git a/src/application/usecases/actions/publish_github_action_workflow.ts b/src/application/usecases/actions/publish_github_action_workflow.ts index b11db622..d0124b51 100644 --- a/src/application/usecases/actions/publish_github_action_workflow.ts +++ b/src/application/usecases/actions/publish_github_action_workflow.ts @@ -1,7 +1,7 @@ import type { Execution } from '../../../data/model/execution'; import { Result } from '../../../data/model/result'; import type { RepositoryReleasePublicationPort, RepositoryTagPort } from '../../ports/repository_release_ports'; -import { INPUT_KEYS } from '../../../utils/constants'; +import { INPUT_KEYS } from '../../contracts/input_keys'; import { logError, logInfo } from '../../ports/logging_ports'; export async function runPublishGithubAction( diff --git a/src/application/usecases/actions/setup_resource_provisioning.ts b/src/application/usecases/actions/setup_resource_provisioning.ts new file mode 100644 index 00000000..f7c5793f --- /dev/null +++ b/src/application/usecases/actions/setup_resource_provisioning.ts @@ -0,0 +1,181 @@ +import type { + SetupConfiguration, + SetupCredentialCollection, + SetupRemoteConfiguration, + SetupResourceTarget, +} from '../../../domain/setup'; +import { + buildSetupRepositoryVariables, + resolveSetupResourceTarget, + shouldUpsertSetupResource, + usesOrganizationStorage, +} from '../../policies/setup_configuration_policy'; +import type { + SetupRemoteConfigurationReadPort, + SetupRepositorySecretsPort, + SetupRepositoryVariablesPort, +} from '../../ports/setup_wizard_ports'; +import { logError } from '../../ports/logging_ports'; + +export interface SetupResourceProvisioningDependencies { + setupRepositoryVariablesPort?: SetupRepositoryVariablesPort; + setupRepositorySecretsPort?: SetupRepositorySecretsPort; + setupRemoteConfigurationReadPort?: SetupRemoteConfigurationReadPort; +} + +export interface SetupRepositoryContext { + owner: string; + repo: string; + token: string; + setupCredentials?: SetupCredentialCollection; + setupRemoteConfiguration?: SetupRemoteConfiguration; +} + +export type SetupResource = { name: string; value: string }; +export type SetupResourceGroup = { target: SetupResourceTarget; resources: SetupResource[] }; + +export async function ensureRepositoryVariables( + context: SetupRepositoryContext, + dependencies: SetupResourceProvisioningDependencies, + setupConfiguration?: SetupConfiguration, + remoteConfiguration?: SetupRemoteConfiguration, +): Promise<{ step?: string; errors: string[] }> { + if (!setupConfiguration?.manageRepositoryVariables || !dependencies.setupRepositoryVariablesPort) { + return { errors: [] }; + } + try { + const desired = buildSetupRepositoryVariables(setupConfiguration); + const groups = groupSetupResources(desired, 'variable', setupConfiguration, remoteConfiguration); + const result = await upsertVariableGroups(context, dependencies.setupRepositoryVariablesPort, groups); + if (result.errors.length > 0) return { errors: result.errors }; + return { + step: `✅ GitHub Actions Variables: ${result.created} created, ${result.updated} updated; existing effective values preserved when no override was selected.`, + errors: [], + }; + } catch (error) { + const message = `Error configuring repository Variables: ${error}`; + logError(message); + return { errors: [message] }; + } +} + +export async function ensureRepositorySecrets( + context: SetupRepositoryContext, + dependencies: SetupResourceProvisioningDependencies, + setupConfiguration?: SetupConfiguration, + remoteConfiguration?: SetupRemoteConfiguration, +): Promise<{ step?: string; errors: string[] }> { + if (!setupConfiguration?.manageRepositorySecrets || !dependencies.setupRepositorySecretsPort) { + return { errors: [] }; + } + const credentials = context.setupCredentials; + if (!credentials) { + return { step: '⚠️ Repository Secrets were not changed: run interactive setup to validate and provide credentials.', errors: [] }; + } + const values = [ + ...(credentials.workflowPat ? [credentials.workflowPat] : []), + ...credentials.apiKeys, + ]; + if (values.length === 0) return { step: '✅ Existing Repository Secrets kept unchanged.', errors: [] }; + try { + const groups = groupSetupResources(values, 'secret', setupConfiguration, remoteConfiguration); + const result = await upsertSecretGroups(context, dependencies.setupRepositorySecretsPort, groups); + if (result.errors.length > 0) return { errors: result.errors }; + return { + step: `✅ GitHub Actions Secrets: ${result.created} created, ${result.updated} updated; existing effective values kept when no replacement was selected.`, + errors: [], + }; + } catch (error) { + const message = `Error configuring repository Secrets: ${error}`; + logError(message); + return { errors: [message] }; + } +} + +export async function resolveRemoteConfiguration( + context: SetupRepositoryContext, + dependencies: SetupResourceProvisioningDependencies, + setupConfiguration: SetupConfiguration | undefined, + errors: string[], +): Promise { + if (context.setupRemoteConfiguration) return context.setupRemoteConfiguration; + if (!dependencies.setupRemoteConfigurationReadPort || !setupConfiguration) return undefined; + try { + return await dependencies.setupRemoteConfigurationReadPort.inspect(context.owner, context.repo, context.token); + } catch (error) { + const message = `Could not inspect existing GitHub Actions resource scopes: ${error instanceof Error ? error.message : String(error)}`; + logError(message); + if (usesOrganizationStorage(setupConfiguration)) errors.push(message); + return undefined; + } +} + +/** Groups resources by their resolved storage target so each provider call is scoped explicitly. */ +export function groupSetupResources( + resources: readonly SetupResource[], + kind: 'secret' | 'variable', + configuration: SetupConfiguration, + remoteConfiguration?: SetupRemoteConfiguration, +): SetupResourceGroup[] { + const groups = new Map(); + for (const resource of resources) { + // Secret values reach this workflow only after the user chose keep/replace. + // Variables are generated from the selected setup contract, so preserving + // an inherited value must happen before the provider call is assembled. + if (kind === 'variable' && !shouldUpsertSetupResource(configuration, kind, resource.name, remoteConfiguration)) continue; + const target = resolveSetupResourceTarget(configuration, kind, resource.name, remoteConfiguration); + const key = `${target.scope}:${target.organizationVisibility}:${target.repositoryId ?? ''}`; + const group = groups.get(key) ?? { target, resources: [] }; + group.resources.push(resource); + groups.set(key, group); + } + return [...groups.values()]; +} + +async function upsertVariableGroups( + context: SetupRepositoryContext, + port: SetupRepositoryVariablesPort, + groups: readonly SetupResourceGroup[], +): Promise<{ created: number; updated: number; errors: string[] }> { + let created = 0; + let updated = 0; + const errors: string[] = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedVariables) { + errors.push('Organization Variable provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedVariables!(context.owner, context.repo, context.token, group.target, group.resources) + : await port.upsert(context.owner, context.repo, context.token, group.resources); + created += result.created; + updated += result.updated; + errors.push(...result.errors); + } + return { created, updated, errors }; +} + +async function upsertSecretGroups( + context: SetupRepositoryContext, + port: SetupRepositorySecretsPort, + groups: readonly SetupResourceGroup[], +): Promise<{ created: number; updated: number; skipped: number; errors: string[] }> { + let created = 0; + let updated = 0; + let skipped = 0; + const errors: string[] = []; + for (const group of groups) { + if (group.target.scope === 'organization' && !port.upsertScopedSecrets) { + errors.push('Organization Secret provisioning is not available in this installation.'); + continue; + } + const result = group.target.scope === 'organization' + ? await port.upsertScopedSecrets!(context.owner, context.repo, context.token, group.target, group.resources) + : await port.upsertSecrets(context.owner, context.repo, context.token, group.resources); + created += result.created; + updated += result.updated; + skipped += result.skipped; + errors.push(...result.errors); + } + return { created, updated, skipped, errors }; +} diff --git a/src/application/usecases/execution/execution_issue_number_policy.ts b/src/application/usecases/execution/execution_issue_number_policy.ts index 350f1b39..443b2c04 100644 --- a/src/application/usecases/execution/execution_issue_number_policy.ts +++ b/src/application/usecases/execution/execution_issue_number_policy.ts @@ -1,4 +1,4 @@ -import { INPUT_KEYS } from '../../../utils/constants'; +import { INPUT_KEYS } from '../../contracts/input_keys'; import { parsePositiveSafeInteger } from '../../../domain/positive_integer_policy'; import { extractIssueNumberFromBranch, extractIssueNumberFromPush } from '../../../utils/title_utils'; import type { ExecutionIssueResolutionContext } from '../../ports/execution_resolution_ports'; diff --git a/src/application/usecases/single_action_use_case.ts b/src/application/usecases/single_action_use_case.ts index a4726338..95ba227a 100644 --- a/src/application/usecases/single_action_use_case.ts +++ b/src/application/usecases/single_action_use_case.ts @@ -18,6 +18,7 @@ export class SingleActionUseCase implements ParamUseCase { private readonly checkProgressUseCase: ParamUseCase, private readonly detectPotentialProblemsUseCase: ParamUseCase, private readonly recommendStepsUseCase: ParamUseCase, + private readonly closeInactiveIssuesUseCase?: ParamUseCase, ) {} async invoke(param: Execution): Promise { @@ -36,6 +37,7 @@ export class SingleActionUseCase implements ParamUseCase { checkProgressUseCase: this.checkProgressUseCase, detectPotentialProblemsUseCase: this.detectPotentialProblemsUseCase, recommendStepsUseCase: this.recommendStepsUseCase, + closeInactiveIssuesUseCase: this.closeInactiveIssuesUseCase, }); } } diff --git a/src/application/usecases/single_action_workflow.ts b/src/application/usecases/single_action_workflow.ts index 2e75ac4e..cbc493fb 100644 --- a/src/application/usecases/single_action_workflow.ts +++ b/src/application/usecases/single_action_workflow.ts @@ -13,6 +13,7 @@ export interface SingleActionWorkflowPorts { checkProgressUseCase: ParamUseCase; detectPotentialProblemsUseCase: ParamUseCase; recommendStepsUseCase: ParamUseCase; + closeInactiveIssuesUseCase?: ParamUseCase; } export async function runSingleActionWorkflow( @@ -38,9 +39,10 @@ export async function runSingleActionWorkflow( { active: param.singleAction.isCheckProgressAction, useCase: ports.checkProgressUseCase }, { active: param.singleAction.isDetectPotentialProblemsAction, useCase: ports.detectPotentialProblemsUseCase }, { active: param.singleAction.isRecommendStepsAction, useCase: ports.recommendStepsUseCase }, - ].find(({ active }) => active); + { active: param.singleAction.isCloseInactiveIssuesAction, useCase: ports.closeInactiveIssuesUseCase }, + ].find(({ active, useCase }) => active && useCase !== undefined); - if (!action) return []; + if (!action || !action.useCase) return []; try { return await action.useCase.invoke(param); diff --git a/src/application/usecases/steps/commit/bugbot/__tests__/limit_comments.test.ts b/src/application/usecases/steps/commit/bugbot/__tests__/limit_comments.test.ts index 47a3d0e8..d98cf31c 100644 --- a/src/application/usecases/steps/commit/bugbot/__tests__/limit_comments.test.ts +++ b/src/application/usecases/steps/commit/bugbot/__tests__/limit_comments.test.ts @@ -2,7 +2,7 @@ * Unit tests for applyCommentLimit: max comments and overflow titles. */ -import { BUGBOT_MAX_COMMENTS } from '../../../../../../utils/constants'; +import { BUGBOT_MAX_COMMENTS } from '../../../../../policies/bugbot_constants'; import { applyCommentLimit } from '../limit_comments'; import type { BugbotFinding } from '../types'; diff --git a/src/application/usecases/steps/commit/bugbot/apply_detected_findings.ts b/src/application/usecases/steps/commit/bugbot/apply_detected_findings.ts index 2978a455..131f5e44 100644 --- a/src/application/usecases/steps/commit/bugbot/apply_detected_findings.ts +++ b/src/application/usecases/steps/commit/bugbot/apply_detected_findings.ts @@ -8,7 +8,7 @@ import { } from "./prepare_bugbot_findings"; import { markFindingsResolved } from "./mark_findings_resolved_use_case"; import { publishFindings } from "./publish_findings_use_case"; -import { BUGBOT_MAX_COMMENTS } from "../../../../../utils/constants"; +import { BUGBOT_MAX_COMMENTS } from '../../../../policies/bugbot_constants'; import { PullRequestReviewOperationError } from "../../../../../application/ports/pull_request_review_errors"; export function prepareDetectedFindings( diff --git a/src/application/usecases/steps/commit/bugbot/limit_comments.ts b/src/application/usecases/steps/commit/bugbot/limit_comments.ts index ddd13374..e7082067 100644 --- a/src/application/usecases/steps/commit/bugbot/limit_comments.ts +++ b/src/application/usecases/steps/commit/bugbot/limit_comments.ts @@ -1,4 +1,4 @@ -import { BUGBOT_MAX_COMMENTS } from "../../../../../utils/constants"; +import { BUGBOT_MAX_COMMENTS } from '../../../../policies/bugbot_constants'; import type { BugbotFinding } from "./types"; export interface ApplyLimitResult { diff --git a/src/application/usecases/steps/commit/bugbot/marker.ts b/src/application/usecases/steps/commit/bugbot/marker.ts index 8d21bb22..d3dfa100 100644 --- a/src/application/usecases/steps/commit/bugbot/marker.ts +++ b/src/application/usecases/steps/commit/bugbot/marker.ts @@ -5,7 +5,7 @@ * threads when the user replies "fix it" in a PR. */ -import { BUGBOT_MARKER_PREFIX } from "../../../../../utils/constants"; +import { BUGBOT_MARKER_PREFIX } from '../../../../policies/bugbot_constants'; import { ApplicationError } from "../../../../errors/application_error"; import type { BugbotFinding, BugbotFindingResolution } from "./types"; import { sanitizeAgentMarkdown } from "../../../../../application/policies/github_comment_publication_policy"; diff --git a/src/architecture/__tests__/production_dependency_boundaries.test.ts b/src/architecture/__tests__/production_dependency_boundaries.test.ts new file mode 100644 index 00000000..1ff82c4d --- /dev/null +++ b/src/architecture/__tests__/production_dependency_boundaries.test.ts @@ -0,0 +1,79 @@ +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; + +function productionTypeScriptFiles(directory: string): string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + return entry.name === '__tests__' ? [] : productionTypeScriptFiles(path); + } + return entry.name.endsWith('.ts') + && !entry.name.endsWith('.test.ts') + && !entry.name.endsWith('.d.ts') + ? [resolve(path)] + : []; + }); +} + +function relativeModuleSpecifiers(source: string): string[] { + const imports = /(?:import|export)\s+(?:type\s+)?(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g; + const runtimeImports = /(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + return [ + ...Array.from(source.matchAll(imports), match => match[1]), + ...Array.from(source.matchAll(runtimeImports), match => match[1]), + ].filter(specifier => specifier.startsWith('.')); +} + +function resolveTypeScriptImport(file: string, specifier: string): string | undefined { + const target = resolve(dirname(file), specifier); + return [`${target}.ts`, join(target, 'index.ts')].find(existsSync); +} + +function layerPath(sourceRoot: string, file: string): string { + return relative(sourceRoot, file).split('/')[1] ?? ''; +} + +function isPurePath(sourceRoot: string, file: string): boolean { + const path = relative(sourceRoot, file); + return path === 'domain' || path.startsWith('domain/') + || path === 'data/model' || path.startsWith('data/model/'); +} + +describe('production dependency boundaries', () => { + const sourceRoot = resolve(__dirname, '../..'); + const files = productionTypeScriptFiles(sourceRoot); + const fileSet = new Set(files); + + it('resolves every relative production import', () => { + const unresolved = files.flatMap(file => relativeModuleSpecifiers(readFileSync(file, 'utf8')) + .filter(specifier => resolveTypeScriptImport(file, specifier) === undefined) + .map(specifier => `${relative(sourceRoot, file)} -> ${specifier}`)); + + expect(unresolved).toEqual([]); + }); + + it('keeps pure model and domain code inside the pure core', () => { + const violations = files + .filter(file => isPurePath(sourceRoot, file)) + .flatMap(file => relativeModuleSpecifiers(readFileSync(file, 'utf8')) + .map(specifier => resolveTypeScriptImport(file, specifier)) + .filter((dependency): dependency is string => dependency !== undefined && fileSet.has(dependency)) + .filter(dependency => !isPurePath(sourceRoot, dependency)) + .map(dependency => `${relative(sourceRoot, file)} -> ${relative(sourceRoot, dependency)}`)); + + expect(violations).toEqual([]); + }); + + it('keeps application code independent of outer runtime layers', () => { + const forbiddenOuterLayers = new Set(['actions', 'cli', 'infrastructure', 'manager']); + const violations = files + .filter(file => layerPath(sourceRoot, file) === 'application') + .flatMap(file => relativeModuleSpecifiers(readFileSync(file, 'utf8')) + .map(specifier => resolveTypeScriptImport(file, specifier)) + .filter((dependency): dependency is string => dependency !== undefined && fileSet.has(dependency)) + .filter(dependency => forbiddenOuterLayers.has(layerPath(sourceRoot, dependency))) + .map(dependency => `${relative(sourceRoot, file)} -> ${relative(sourceRoot, dependency)}`)); + + expect(violations).toEqual([]); + }); +}); diff --git a/src/cli/__tests__/setup_prompt_rendering.test.ts b/src/cli/__tests__/setup_prompt_rendering.test.ts new file mode 100644 index 00000000..b15f3d94 --- /dev/null +++ b/src/cli/__tests__/setup_prompt_rendering.test.ts @@ -0,0 +1,88 @@ +import { + color, + doctorIcon, + formatTask, + renderBox, + renderRemoteConfiguration, + statusIcon, +} from '../setup_prompt_rendering'; + +describe('setup prompt rendering', () => { + it.each([ + ['valid', '✓'], + ['unverifiable', '?'], + ['missing', '!'], + ['not_required', '–'], + ['invalid', '✗'], + ] as const)('maps credential status %s to %s', (status, expected) => { + expect(statusIcon(status)).toBe(expected); + }); + + it.each([ + ['pass', '✓'], + ['warn', '⚠'], + ['fail', '✗'], + ] as const)('maps doctor status %s to %s', (status, expected) => { + expect(doctorIcon(status)).toBe(expected); + }); + + it('formats task labels and leaves non-TTY text uncolored', () => { + expect(formatTask('planner')).toBe('Planner'); + expect(formatTask('')).toBe(''); + expect(color('text', 36)).toBe('text'); + }); + + it('renders a bordered box with a title and content', () => { + const rendered = renderBox('first\nsecond', 'Setup', 32); + + expect(rendered).toContain('Setup'); + expect(rendered).toContain('first'); + expect(rendered).toContain('second'); + expect(rendered.split('\n')[0]).toMatch(/^╭─+╮$/); + }); + + it('renders remote metadata without exposing credential values', () => { + const rendered = renderRemoteConfiguration( + { + ownerType: 'Organization', + repositoryId: 42, + repositoryVisibility: 'private', + repositorySecrets: ['PAT'], + organizationSecrets: ['OPENAI_API_KEY'], + repositoryVariables: [{ name: 'AGENT_MODEL', value: 'gpt-5.6' }], + organizationVariables: [{ name: 'AGENT_PROVIDER', value: 'codex' }], + organizationAccess: 'available', + organizationSecretsAccess: 'available', + organizationVariablesAccess: 'available', + }, + [{ name: 'AGENT_MODEL', value: 'gpt-5.6' }], + [{ name: 'PAT', kind: 'workflowPat', description: 'workflow token' }], + ); + + expect(rendered).toContain('Organization resources can be inspected'); + expect(rendered).toContain('PAT'); + expect(rendered).not.toContain('credential-value'); + }); + + it('renders empty remote collections and unavailable organization access', () => { + const rendered = renderRemoteConfiguration( + { + ownerType: 'User', + repositoryVisibility: 'unknown', + repositorySecrets: [], + organizationSecrets: [], + repositoryVariables: [], + organizationVariables: [], + organizationAccess: 'unavailable', + organizationSecretsAccess: 'unavailable', + organizationVariablesAccess: 'unavailable', + }, + [], + [], + ); + + expect(rendered).toContain('repository ID: unknown'); + expect(rendered).toContain('(none detected)'); + expect(rendered).toContain('Organization resource inspection: unavailable.'); + }); +}); diff --git a/src/cli/cli_errors.ts b/src/cli/cli_errors.ts new file mode 100644 index 00000000..94509ac3 --- /dev/null +++ b/src/cli/cli_errors.ts @@ -0,0 +1,3 @@ +export const ERRORS = { + GIT_REPOSITORY_NOT_FOUND: '❌ Git repository not found', +} as const; diff --git a/src/cli/commands/__tests__/detect_potential_problems_policy.test.ts b/src/cli/commands/__tests__/detect_potential_problems_policy.test.ts index 83e6d14e..99ab1b10 100644 --- a/src/cli/commands/__tests__/detect_potential_problems_policy.test.ts +++ b/src/cli/commands/__tests__/detect_potential_problems_policy.test.ts @@ -1,4 +1,5 @@ -import { ACTIONS, INPUT_KEYS } from '../../../utils/constants'; +import { ACTIONS } from '../../../data/model/action_types'; +import { INPUT_KEYS } from '../../../application/contracts/input_keys'; import { buildDetectPotentialProblemsParams, resolveDetectIssueNumber } from '../detect_potential_problems_policy'; const gitInfo = { owner: 'owner', repo: 'repo' } as const; diff --git a/src/cli/commands/__tests__/issue_command_policy.test.ts b/src/cli/commands/__tests__/issue_command_policy.test.ts index 7b6276da..9e8a262e 100644 --- a/src/cli/commands/__tests__/issue_command_policy.test.ts +++ b/src/cli/commands/__tests__/issue_command_policy.test.ts @@ -1,5 +1,6 @@ import { buildCheckProgressParams, buildRecommendStepsParams, parseIssueNumber } from '../issue_command_policy'; -import { ACTIONS, INPUT_KEYS } from '../../../utils/constants'; +import { ACTIONS } from '../../../data/model/action_types'; +import { INPUT_KEYS } from '../../../application/contracts/input_keys'; const gitInfo = { owner: 'owner', repo: 'repo' } as const; diff --git a/src/cli/commands/__tests__/setup_policy.test.ts b/src/cli/commands/__tests__/setup_policy.test.ts index 00cc6e6a..6ab4285e 100644 --- a/src/cli/commands/__tests__/setup_policy.test.ts +++ b/src/cli/commands/__tests__/setup_policy.test.ts @@ -1,4 +1,5 @@ -import { ACTIONS, INPUT_KEYS } from '../../../utils/constants'; +import { ACTIONS } from '../../../data/model/action_types'; +import { INPUT_KEYS } from '../../../application/contracts/input_keys'; import { buildSetupParams } from '../setup_policy'; const gitInfo = { owner: 'owner', repo: 'repo' } as const; diff --git a/src/cli/commands/check_progress.ts b/src/cli/commands/check_progress.ts index 677e8279..85461c50 100644 --- a/src/cli/commands/check_progress.ts +++ b/src/cli/commands/check_progress.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { runLocalAction } from '../../actions/local_action'; -import { TITLE } from '../../utils/constants'; +import { TITLE } from '../../application/contracts/product_identity'; import { logError } from '../../utils/logger'; import { getGitInfo } from '../../cli_context'; import { cleanCliArgument } from '../command_input_policy'; diff --git a/src/cli/commands/detect_potential_problems.ts b/src/cli/commands/detect_potential_problems.ts index 66ae5e00..35f35b10 100644 --- a/src/cli/commands/detect_potential_problems.ts +++ b/src/cli/commands/detect_potential_problems.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { runLocalAction } from '../../actions/local_action'; -import { TITLE } from '../../utils/constants'; +import { TITLE } from '../../application/contracts/product_identity'; import { logError } from '../../utils/logger'; import { getGitInfo, getCurrentBranch } from '../../cli_context'; import { cleanCliArgument } from '../command_input_policy'; diff --git a/src/cli/commands/detect_potential_problems_policy.ts b/src/cli/commands/detect_potential_problems_policy.ts index 9639a579..c1aece28 100644 --- a/src/cli/commands/detect_potential_problems_policy.ts +++ b/src/cli/commands/detect_potential_problems_policy.ts @@ -1,4 +1,5 @@ -import { ACTIONS, INPUT_KEYS } from '../../utils/constants'; +import { ACTIONS } from '../../data/model/action_types'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; import type { GitInfo } from '../../cli_context'; import { cleanCliArgument, parsePositiveCliInteger } from '../command_input_policy'; diff --git a/src/cli/commands/do.ts b/src/cli/commands/do.ts index 03cab45d..68cbe91a 100644 --- a/src/cli/commands/do.ts +++ b/src/cli/commands/do.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { TITLE } from '../../utils/constants'; +import { TITLE } from '../../application/contracts/product_identity'; import { runDoCommand, type DoCommandOptions } from './do_command_handler'; export function registerDoCommand(program: Command): void { diff --git a/src/cli/commands/issue_command_policy.ts b/src/cli/commands/issue_command_policy.ts index 5241e828..7b43fe23 100644 --- a/src/cli/commands/issue_command_policy.ts +++ b/src/cli/commands/issue_command_policy.ts @@ -1,4 +1,5 @@ -import { ACTIONS, INPUT_KEYS } from '../../utils/constants'; +import { ACTIONS } from '../../data/model/action_types'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; import type { GitInfo } from '../../cli_context'; import { cleanCliArgument, parsePositiveCliInteger } from '../command_input_policy'; diff --git a/src/cli/commands/recommend_steps.ts b/src/cli/commands/recommend_steps.ts index 09cdd702..e0ebe3f7 100644 --- a/src/cli/commands/recommend_steps.ts +++ b/src/cli/commands/recommend_steps.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { runLocalAction } from '../../actions/local_action'; -import { TITLE } from '../../utils/constants'; +import { TITLE } from '../../application/contracts/product_identity'; import { logError } from '../../utils/logger'; import { getGitInfo } from '../../cli_context'; import { cleanCliArgument } from '../command_input_policy'; diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index 1495d470..449b60c3 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { runLocalAction } from '../../actions/local_action'; -import { TITLE } from '../../utils/constants'; +import { TITLE } from '../../application/contracts/product_identity'; import { getSetupToken } from '../../utils/setup_files'; import { logError, logInfo } from '../../utils/logger'; import { getGitInfo, isInsideGitRepo } from '../../cli_context'; diff --git a/src/cli/commands/setup_policy.ts b/src/cli/commands/setup_policy.ts index 3ce7333c..11ff3724 100644 --- a/src/cli/commands/setup_policy.ts +++ b/src/cli/commands/setup_policy.ts @@ -1,4 +1,5 @@ -import { ACTIONS, INPUT_KEYS } from '../../utils/constants'; +import { ACTIONS } from '../../data/model/action_types'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; import type { GitInfo } from '../../cli_context'; import type { SetupConfiguration, SetupCredentialCollection, SetupRemoteConfiguration } from '../../domain/setup'; import { buildSetupActionInputs } from '../../application/policies/setup_configuration_policy'; diff --git a/src/cli/commands/think.ts b/src/cli/commands/think.ts index 06ec747f..09a77bc8 100644 --- a/src/cli/commands/think.ts +++ b/src/cli/commands/think.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { TITLE } from "../../utils/constants"; +import { TITLE } from '../../application/contracts/product_identity'; import { runThinkCommand, type ThinkCommandOptions } from "./think_command_handler"; export function registerThinkCommand(program: Command): void { diff --git a/src/cli/commands/think_command_handler.ts b/src/cli/commands/think_command_handler.ts index 33f25b03..44c8e5d6 100644 --- a/src/cli/commands/think_command_handler.ts +++ b/src/cli/commands/think_command_handler.ts @@ -1,6 +1,7 @@ import { runLocalAction } from "../../actions/local_action"; import { createIssueMetadataCompositionRoot } from "../../infrastructure/composition/issue_metadata_composition_root"; -import { ACTIONS, INPUT_KEYS } from "../../utils/constants"; +import { ACTIONS } from '../../data/model/action_types'; +import { INPUT_KEYS } from '../../application/contracts/input_keys'; import { logError } from "../../utils/logger"; import { getGitInfo } from "../../cli_context"; import { cleanCliArgument, joinCliArguments } from "../command_input_policy"; diff --git a/src/cli/setup_config_file.ts b/src/cli/setup_config_file.ts index 3c921bb0..b274f82b 100644 --- a/src/cli/setup_config_file.ts +++ b/src/cli/setup_config_file.ts @@ -33,7 +33,7 @@ const REPOSITORY_STRING_KEYS = new Set([ 'commitPrefixTransforms', ]); const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush']); -const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout']); +const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout', 'inactivityThresholdHours']); const AI_BOOLEAN_KEYS = new Set(['pullRequestDescription', 'membersOnly', 'includeReasoning']); const AI_STRING_KEYS = new Set(['ignoreFiles', 'pullRequestDescriptionMode', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'provisioningMode']); const AI_NUMBER_KEYS = new Set(['bugbotCommentLimit']); diff --git a/src/cli/setup_prompt_adapter.ts b/src/cli/setup_prompt_adapter.ts index 530aeea6..d6c55f68 100644 --- a/src/cli/setup_prompt_adapter.ts +++ b/src/cli/setup_prompt_adapter.ts @@ -25,6 +25,14 @@ import type { SetupRemoteConfiguration, SetupVariable, } from '../domain/setup'; +import { + color, + doctorIcon, + formatTask, + renderBox, + renderRemoteConfiguration, + statusIcon, +} from './setup_prompt_rendering'; const AGENT_PROVIDERS = ['codex', 'opencode', 'cursor'] as const; const MODEL_PROVIDERS = ['openai', 'anthropic', 'google', 'openrouter', 'opencode', 'local'] as const; @@ -103,6 +111,7 @@ export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromp repository.desiredAssigneesCount = await this.askNumber('Desired issue assignees (0 disables automatic assignment)', repository.desiredAssigneesCount); repository.desiredReviewersCount = await this.askNumber('Desired pull-request reviewers (0 disables automatic assignment)', repository.desiredReviewersCount); repository.mergeTimeout = await this.askNumber('Merge timeout in seconds (0 disables the timeout)', repository.mergeTimeout); + repository.inactivityThresholdHours = await this.askNumber('Hours without activity before closing a waiting issue', repository.inactivityThresholdHours); repository.issueLocale = await this.askText('Issue comment locale', repository.issueLocale); repository.pullRequestLocale = await this.askText('Pull-request comment locale', repository.pullRequestLocale); repository.commitPrefixTransforms = await this.askText('Commit prefix transforms', repository.commitPrefixTransforms); @@ -389,61 +398,3 @@ export class SetupPromptAdapter implements SetupPromptPort, SetupCredentialPromp return { defaultScope, organizationVisibility, preserveExisting, overrides }; } } - -function statusIcon(status: SetupCredentialCheck['status']): string { - if (status === 'valid') return '✓'; - if (status === 'unverifiable') return '?'; - if (status === 'missing') return '!'; - if (status === 'not_required') return '–'; - return '✗'; -} - -function doctorIcon(status: import('../domain/setup').DoctorCheckStatus): string { - return status === 'pass' ? '✓' : status === 'warn' ? '⚠' : '✗'; -} - -function formatTask(task: string): string { - return task.charAt(0).toUpperCase() + task.slice(1); -} - -function color(value: string, code: number): string { - if (!stdout.isTTY) return value; - return `\u001b[${code}m${value}\u001b[0m`; -} - -function renderBox(content: string, title: string, borderCode = 36): string { - const lines = [` ${title} `, ...content.split('\n').map(line => ` ${line}`)]; - const width = Math.max(...lines.map(line => stripAnsi(line).length)) + 1; - const border = color(`╭${'─'.repeat(width)}╮`, borderCode); - const bottom = color(`╰${'─'.repeat(width)}╯`, borderCode); - return [ - border, - ...lines.map(line => `${color('│', borderCode)}${line}${' '.repeat(Math.max(0, width - stripAnsi(line).length))}${color('│', borderCode)}`), - bottom, - ].join('\n'); -} - -function renderRemoteConfiguration( - remote: SetupRemoteConfiguration, - variables: readonly SetupVariable[], - requirements: readonly SetupCredentialRequirement[], -): string { - const lines = [ - `Target owner: ${remote.ownerType}; repository visibility: ${remote.repositoryVisibility}; repository ID: ${remote.repositoryId ?? 'unknown'}`, - `Repository Secrets: ${remote.repositorySecrets.length > 0 ? remote.repositorySecrets.join(', ') : '(none detected)'}`, - `Organization Secrets available here: ${remote.organizationSecrets.length > 0 ? remote.organizationSecrets.join(', ') : '(none detected)'}`, - `Repository Variables: ${remote.repositoryVariables.length > 0 ? remote.repositoryVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, - `Organization Variables available here: ${remote.organizationVariables.length > 0 ? remote.organizationVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, - `Required Secrets: ${requirements.map(requirement => requirement.name).join(', ')}`, - `Required Variables: ${variables.map(variable => variable.name).join(', ')}`, - remote.organizationAccess === 'available' - ? 'Organization resources can be inspected for this repository.' - : `Organization resource inspection: ${remote.organizationAccess}.`, - 'Repository-level resources take precedence over organization-level resources. Secret values are never displayed.', - ]; - return lines.join('\n'); -} - -function stripAnsi(value: string): string { - return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''); -} diff --git a/src/cli/setup_prompt_rendering.ts b/src/cli/setup_prompt_rendering.ts new file mode 100644 index 00000000..5db84b93 --- /dev/null +++ b/src/cli/setup_prompt_rendering.ts @@ -0,0 +1,66 @@ +import { stdout } from 'node:process'; +import type { + DoctorCheckStatus, + SetupCredentialCheck, + SetupCredentialRequirement, + SetupRemoteConfiguration, + SetupVariable, +} from '../domain/setup'; + +export function statusIcon(status: SetupCredentialCheck['status']): string { + if (status === 'valid') return '✓'; + if (status === 'unverifiable') return '?'; + if (status === 'missing') return '!'; + if (status === 'not_required') return '–'; + return '✗'; +} + +export function doctorIcon(status: DoctorCheckStatus): string { + return status === 'pass' ? '✓' : status === 'warn' ? '⚠' : '✗'; +} + +export function formatTask(task: string): string { + return task.charAt(0).toUpperCase() + task.slice(1); +} + +export function color(value: string, code: number): string { + if (!stdout.isTTY) return value; + return `\u001b[${code}m${value}\u001b[0m`; +} + +export function renderBox(content: string, title: string, borderCode = 36): string { + const lines = [` ${title} `, ...content.split('\n').map(line => ` ${line}`)]; + const width = Math.max(...lines.map(line => stripAnsi(line).length)) + 1; + const border = color(`╭${'─'.repeat(width)}╮`, borderCode); + const bottom = color(`╰${'─'.repeat(width)}╯`, borderCode); + return [ + border, + ...lines.map(line => `${color('│', borderCode)}${line}${' '.repeat(Math.max(0, width - stripAnsi(line).length))}${color('│', borderCode)}`), + bottom, + ].join('\n'); +} + +export function renderRemoteConfiguration( + remote: SetupRemoteConfiguration, + variables: readonly SetupVariable[], + requirements: readonly SetupCredentialRequirement[], +): string { + const lines = [ + `Target owner: ${remote.ownerType}; repository visibility: ${remote.repositoryVisibility}; repository ID: ${remote.repositoryId ?? 'unknown'}`, + `Repository Secrets: ${remote.repositorySecrets.length > 0 ? remote.repositorySecrets.join(', ') : '(none detected)'}`, + `Organization Secrets available here: ${remote.organizationSecrets.length > 0 ? remote.organizationSecrets.join(', ') : '(none detected)'}`, + `Repository Variables: ${remote.repositoryVariables.length > 0 ? remote.repositoryVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, + `Organization Variables available here: ${remote.organizationVariables.length > 0 ? remote.organizationVariables.map(variable => variable.name).join(', ') : '(none detected)'}`, + `Required Secrets: ${requirements.map(requirement => requirement.name).join(', ')}`, + `Required Variables: ${variables.map(variable => variable.name).join(', ')}`, + remote.organizationAccess === 'available' + ? 'Organization resources can be inspected for this repository.' + : `Organization resource inspection: ${remote.organizationAccess}.`, + 'Repository-level resources take precedence over organization-level resources. Secret values are never displayed.', + ]; + return lines.join('\n'); +} + +function stripAnsi(value: string): string { + return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''); +} diff --git a/src/cli_context.ts b/src/cli_context.ts index 13cfb1df..0c0a479c 100644 --- a/src/cli_context.ts +++ b/src/cli_context.ts @@ -1,5 +1,5 @@ import { execSync } from 'child_process'; -import { ERRORS } from './utils/constants'; +import { ERRORS } from './cli/cli_errors'; export type GitInfo = { owner: string; repo: string } | { error: string }; diff --git a/src/data/model/__tests__/execution.test.ts b/src/data/model/__tests__/execution.test.ts index bd97fdbb..8f4b0ae7 100644 --- a/src/data/model/__tests__/execution.test.ts +++ b/src/data/model/__tests__/execution.test.ts @@ -28,7 +28,8 @@ const mockGetReleaseVersionInvoke = jest.fn(); const mockGetReleaseTypeInvoke = jest.fn(); const mockGetHotfixVersionInvoke = jest.fn(); -import { ACTIONS, INPUT_KEYS } from '../../../utils/constants'; +import { ACTIONS } from '../action_types'; +import { INPUT_KEYS } from '../../../application/contracts/input_keys'; import { Ai } from '../ai'; import { Branches } from '../branches'; import { Emoji } from '../emoji'; diff --git a/src/data/model/__tests__/initial_labels_policy.test.ts b/src/data/model/__tests__/initial_labels_policy.test.ts index a274db43..e2381a79 100644 --- a/src/data/model/__tests__/initial_labels_policy.test.ts +++ b/src/data/model/__tests__/initial_labels_policy.test.ts @@ -1,4 +1,4 @@ -import { ACTIONS } from '../../../utils/constants'; +import { ACTIONS } from '../action_types'; import { shouldSkipInitialLabelsFetch } from '../initial_labels_policy'; describe('initial labels policy', () => { diff --git a/src/data/model/__tests__/single_action.test.ts b/src/data/model/__tests__/single_action.test.ts index 4605bf40..4131cc62 100644 --- a/src/data/model/__tests__/single_action.test.ts +++ b/src/data/model/__tests__/single_action.test.ts @@ -1,4 +1,4 @@ -import { ACTIONS } from '../../../utils/constants'; +import { ACTIONS } from '../action_types'; import { SingleAction } from '../single_action'; describe('SingleAction', () => { @@ -45,8 +45,15 @@ describe('SingleAction', () => { }); it('isRecommendStepsAction', () => { - const s = new SingleAction(ACTIONS.RECOMMEND_STEPS, '5', '', '', ''); - expect(s.isRecommendStepsAction).toBe(true); + const s = new SingleAction(ACTIONS.RECOMMEND_STEPS, '5', '', '', ''); + expect(s.isRecommendStepsAction).toBe(true); + }); + + it('isCloseInactiveIssuesAction', () => { + const s = new SingleAction(ACTIONS.CLOSE_INACTIVE_ISSUES, '0', '', '', ''); + expect(s.isCloseInactiveIssuesAction).toBe(true); + expect(s.validSingleAction).toBe(true); + expect(s.isSingleActionWithoutIssue).toBe(true); }); }); diff --git a/src/data/model/action_types.ts b/src/data/model/action_types.ts index f325d0b2..d975c795 100644 --- a/src/data/model/action_types.ts +++ b/src/data/model/action_types.ts @@ -9,4 +9,5 @@ export const ACTIONS = { CHECK_PROGRESS: 'check_progress_action', DETECT_POTENTIAL_PROBLEMS: 'detect_potential_problems_action', RECOMMEND_STEPS: 'recommend_steps_action', + CLOSE_INACTIVE_ISSUES: 'close_inactive_issues_action', } as const; diff --git a/src/data/model/execution.ts b/src/data/model/execution.ts index 55e02b4f..8dc13959 100644 --- a/src/data/model/execution.ts +++ b/src/data/model/execution.ts @@ -22,6 +22,7 @@ import { Workflows } from "./workflows"; import { githubUsersMatch } from '../../domain/github_user_policy'; import type { ExecutionInputs } from './execution_inputs'; import type { ExecutionComponents } from './execution_components'; +import { DEFAULT_INACTIVITY_THRESHOLD_HOURS } from '../../domain/issue_inactivity'; export class Execution { @@ -55,6 +56,7 @@ export class Execution { previousConfiguration: Config | undefined; currentConfiguration: Config; tokenUser: string | undefined; + inactivityThresholdHours: number; inputs: ExecutionInputs | undefined; get eventName(): string { @@ -183,6 +185,7 @@ export class Execution { this.project = components.projects; this.workflows = components.workflows; this.tokenUser = components.tokenUser; + this.inactivityThresholdHours = components.inactivityThresholdHours ?? DEFAULT_INACTIVITY_THRESHOLD_HOURS; this.currentConfiguration = new Config({}); this.inputs = components.inputs; this.welcome = components.welcome; diff --git a/src/data/model/execution_components.ts b/src/data/model/execution_components.ts index 6a0840b4..91cbc47f 100644 --- a/src/data/model/execution_components.ts +++ b/src/data/model/execution_components.ts @@ -39,5 +39,6 @@ export interface ExecutionComponents { projects: Projects; tokenUser?: string; welcome?: Welcome; + inactivityThresholdHours?: number; inputs?: ExecutionInputs; } diff --git a/src/data/model/single_action.ts b/src/data/model/single_action.ts index d512f765..5f01ef5e 100644 --- a/src/data/model/single_action.ts +++ b/src/data/model/single_action.ts @@ -13,6 +13,7 @@ export class SingleAction { ACTIONS.CHECK_PROGRESS, ACTIONS.DETECT_POTENTIAL_PROBLEMS, ACTIONS.RECOMMEND_STEPS, + ACTIONS.CLOSE_INACTIVE_ISSUES, ]; /** * Actions that throw an error if the last step failed @@ -22,6 +23,7 @@ export class SingleAction { ACTIONS.CREATE_RELEASE, ACTIONS.DEPLOYED, ACTIONS.CREATE_TAG, + ACTIONS.CLOSE_INACTIVE_ISSUES, ]; /** @@ -30,6 +32,7 @@ export class SingleAction { actionsWithoutIssue: string[] = [ ACTIONS.THINK, ACTIONS.INITIAL_SETUP, + ACTIONS.CLOSE_INACTIVE_ISSUES, ]; isIssue: boolean = false; @@ -80,6 +83,10 @@ export class SingleAction { return this.currentSingleAction === ACTIONS.RECOMMEND_STEPS; } + get isCloseInactiveIssuesAction(): boolean { + return this.currentSingleAction === ACTIONS.CLOSE_INACTIVE_ISSUES; + } + get enabledSingleAction(): boolean { return this.currentSingleAction.length > 0; } diff --git a/src/data/repository/ai/agent_capability_adapter.ts b/src/data/repository/ai/agent_capability_adapter.ts index 6c8afce7..c14dbb1f 100644 --- a/src/data/repository/ai/agent_capability_adapter.ts +++ b/src/data/repository/ai/agent_capability_adapter.ts @@ -1,4 +1,4 @@ -import { AGENT_REQUEST_TIMEOUT_MS } from '../../../utils/constants'; +import { AGENT_REQUEST_TIMEOUT_MS } from './agent_constants'; import { logError } from '../../../utils/logger'; import { ProviderCliAdapter } from '../provider_cli_adapter'; diff --git a/src/data/repository/ai/agent_constants.ts b/src/data/repository/ai/agent_constants.ts new file mode 100644 index 00000000..3df3b54b --- /dev/null +++ b/src/data/repository/ai/agent_constants.ts @@ -0,0 +1,2 @@ +/** Maximum time allowed for one external agent CLI request. */ +export const AGENT_REQUEST_TIMEOUT_MS = 900_000; diff --git a/src/data/repository/issue/__tests__/issue_inactivity_repository.test.ts b/src/data/repository/issue/__tests__/issue_inactivity_repository.test.ts new file mode 100644 index 00000000..492d216c --- /dev/null +++ b/src/data/repository/issue/__tests__/issue_inactivity_repository.test.ts @@ -0,0 +1,113 @@ +import { IssueInactivityRepository } from '../issue_inactivity_repository'; +import type { GithubIssueInactivityClient } from '../../../../infrastructure/github/ports/github_issue_provider_ports'; + +describe('IssueInactivityRepository', () => { + it('lists open issues by waiting label and maps issue activity metadata', async () => { + const listForRepo = jest.fn(); + const iterator = jest.fn(async function* () { + yield { + data: [ + { + number: 42, + updated_at: '2026-08-28T00:00:00.000Z', + labels: [{ name: 'state:awaiting-maintainer' }, 'bug'], + }, + { + number: 43, + updated_at: null, + pull_request: {}, + labels: [], + }, + ], + }; + }); + const client: GithubIssueInactivityClient = { + paginate: { + iterator, + }, + rest: { + issues: { + listForRepo, + get: jest.fn(), + }, + }, + }; + const repository = new IssueInactivityRepository({ getClient: () => client }); + + const issues = await repository.listOpenIssuesByLabel('owner', 'repo', 'state:awaiting-maintainer', 'token'); + + expect(iterator).toHaveBeenCalledWith(listForRepo, { + owner: 'owner', + repo: 'repo', + state: 'open', + labels: 'state:awaiting-maintainer', + sort: 'updated', + direction: 'asc', + per_page: 100, + }); + expect(issues).toEqual([ + { + number: 42, + updatedAt: '2026-08-28T00:00:00.000Z', + isPullRequest: false, + labels: ['state:awaiting-maintainer', 'bug'], + }, + { + number: 43, + updatedAt: undefined, + isPullRequest: true, + labels: [], + }, + ]); + }); + + it('returns no issue when the revalidation request is already closed', async () => { + const get = jest.fn().mockResolvedValue({ data: { number: 42, state: 'closed' } }); + const client = { + paginate: { iterator: async function* () {} }, + rest: { issues: { listForRepo: jest.fn(), get } }, + } as unknown as GithubIssueInactivityClient; + const repository = new IssueInactivityRepository({ getClient: () => client }); + + await expect(repository.getOpenIssue('owner', 'repo', 42, 'token')).resolves.toBeUndefined(); + expect(get).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', issue_number: 42 }); + }); + + it('maps an open issue returned during revalidation', async () => { + const get = jest.fn().mockResolvedValue({ + data: { + number: 42, + state: 'open', + updated_at: '2026-08-28T00:00:00.000Z', + labels: [{ name: 'state:awaiting-issue-author' }], + }, + }); + const client = { + paginate: { iterator: async function* () {} }, + rest: { issues: { listForRepo: jest.fn(), get } }, + } as unknown as GithubIssueInactivityClient; + const repository = new IssueInactivityRepository({ getClient: () => client }); + + await expect(repository.getOpenIssue('owner', 'repo', 42, 'token')).resolves.toEqual({ + number: 42, + updatedAt: '2026-08-28T00:00:00.000Z', + isPullRequest: false, + labels: ['state:awaiting-issue-author'], + }); + }); + + it('fails closed when GitHub returns an invalid issue number', async () => { + const client: GithubIssueInactivityClient = { + paginate: { + iterator: async function* () { + yield { data: [{ number: 0 }] }; + }, + }, + rest: { issues: { listForRepo: jest.fn(), get: jest.fn() } }, + }; + const repository = new IssueInactivityRepository({ getClient: () => client }); + + await expect(repository.listOpenIssuesByLabel('owner', 'repo', 'state:awaiting-maintainer', 'token')) + .rejects.toThrow('invalid issue number'); + }); +}); diff --git a/src/data/repository/issue/issue_inactivity_repository.ts b/src/data/repository/issue/issue_inactivity_repository.ts new file mode 100644 index 00000000..07ee3b3c --- /dev/null +++ b/src/data/repository/issue/issue_inactivity_repository.ts @@ -0,0 +1,67 @@ +import type { IssueInactivityQueryPort } from '../../../application/ports/issue_inactivity_ports'; +import type { IssueActivitySnapshot } from '../../../domain/issue_inactivity'; +import type { GithubClientPort } from '../../../infrastructure/github/ports/github_client_provider_port'; +import type { GithubIssueActivity, GithubIssueInactivityClient } from '../../../infrastructure/github/ports/github_issue_provider_ports'; +import { requireArrayPage } from '../github/github_pagination_policy'; + +/** Reads the provider's issue activity timestamp and waiting-state labels. */ +export class IssueInactivityRepository implements IssueInactivityQueryPort { + constructor(private readonly githubClient: GithubClientPort) {} + + listOpenIssuesByLabel = async ( + owner: string, + repository: string, + label: string, + token: string, + ): Promise => { + const client = this.githubClient.getClient(token); + const issues: IssueActivitySnapshot[] = []; + for await (const response of client.paginate.iterator( + client.rest.issues.listForRepo, + { + owner, + repo: repository, + state: 'open', + labels: label, + sort: 'updated', + direction: 'asc', + per_page: 100, + }, + )) { + const page = requireArrayPage(response.data, 'open issues'); + issues.push(...page.map(toSnapshot)); + } + return issues; + }; + + getOpenIssue = async ( + owner: string, + repository: string, + issueNumber: number, + token: string, + ): Promise => { + const client = this.githubClient.getClient(token); + const response = await client.rest.issues.get({ + owner, + repo: repository, + issue_number: issueNumber, + }); + if (response.data.state !== 'open') return undefined; + return toSnapshot(response.data); + }; +} + +function toSnapshot(issue: GithubIssueActivity): IssueActivitySnapshot { + if (!Number.isSafeInteger(issue.number) || issue.number < 1) { + throw new Error('GitHub issue response contained an invalid issue number.'); + } + return { + number: issue.number, + updatedAt: issue.updated_at ?? undefined, + isPullRequest: issue.pull_request !== undefined, + labels: (issue.labels ?? []).flatMap(label => { + const name = typeof label === 'string' ? label : label.name; + return name?.trim() ? [name] : []; + }), + }; +} diff --git a/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts b/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts index 15f685e8..3959f877 100644 --- a/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts +++ b/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts @@ -1,4 +1,4 @@ -import { WORKFLOW_STATUS } from '../../../../utils/constants'; +import { WORKFLOW_STATUS } from '../workflow_status'; import type { GithubWorkflowRun, GithubWorkflowRunsClient, @@ -6,7 +6,7 @@ import type { } from '../../../../infrastructure/github/ports/github_workflow_provider_ports'; import { ActivePreviousWorkflowRunsRepository } from '../active_previous_workflow_runs_repository'; import { COPILOT_WORKFLOW_NAMES } from '../../../../application/policies/workflow_queue_policy'; -import { WORKFLOW_ACTIVE_STATUSES } from '../../../../utils/constants'; +import { WORKFLOW_ACTIVE_STATUSES } from '../workflow_status'; const listWorkflowRunsForRepo = jest.fn(); const listWorkflowRuns = jest.fn(); @@ -83,7 +83,7 @@ describe('ActivePreviousWorkflowRunsRepository', () => { expect(iterator).toHaveBeenCalledTimes(1); }); - it('counts all seven shared workflow names and five active statuses across every page', async () => { + it('counts all eight shared workflow names and five active statuses across every page', async () => { const runs = COPILOT_WORKFLOW_NAMES.flatMap((name, nameIndex) => WORKFLOW_ACTIVE_STATUSES.map((status, statusIndex) => workflowRun({ id: 1 + nameIndex * WORKFLOW_ACTIVE_STATUSES.length + statusIndex, name, @@ -234,4 +234,4 @@ describe('ActivePreviousWorkflowRunsRepository', () => { expect(traversals).toBe(2); expect(retryDelayPort.wait).toHaveBeenCalledWith(10); }); -}); \ No newline at end of file +}); diff --git a/src/data/repository/workflow/active_previous_workflow_runs_repository.ts b/src/data/repository/workflow/active_previous_workflow_runs_repository.ts index ca7024be..3488bd37 100644 --- a/src/data/repository/workflow/active_previous_workflow_runs_repository.ts +++ b/src/data/repository/workflow/active_previous_workflow_runs_repository.ts @@ -12,7 +12,7 @@ import type { GithubWorkflowRun, GithubWorkflowRunsResponse, } from '../../../infrastructure/github/ports/github_workflow_provider_ports'; -import { WORKFLOW_ACTIVE_STATUSES } from '../../../utils/constants'; +import { WORKFLOW_ACTIVE_STATUSES } from './workflow_status'; import { withWorkflowRunsRetry, WORKFLOW_RUNS_RETRY_POLICY, type WorkflowRunsRetryPolicy } from './workflow_runs_retry'; const NO_OP_DELAY_PORT: WorkflowPollingDelayPort = { wait: async () => undefined }; @@ -58,7 +58,7 @@ export class ActivePreviousWorkflowRunsRepository implements PreviousWorkflowRun return withWorkflowRunsRetry(async () => { let activeRunCount = 0; // Keep one complete sequential traversal: GitHub cannot safely express - // the seven shared workflow names, five active statuses, or the strict + // the eight shared workflow names, five active statuses, or the strict // lower-ID predicate in this endpoint. Do not add provider filters or // early-stop on page order; a matching run may occur on a later page. // The residual cost is deep-history pagination, with retries restarting @@ -97,4 +97,4 @@ function isActivePreviousRun( && workflowNames.includes(run.name) && run.id < query.currentRunId && WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); -} \ No newline at end of file +} diff --git a/src/data/repository/workflow/workflow_status.ts b/src/data/repository/workflow/workflow_status.ts new file mode 100644 index 00000000..d3e579a9 --- /dev/null +++ b/src/data/repository/workflow/workflow_status.ts @@ -0,0 +1,20 @@ +export const WORKFLOW_STATUS = { + IN_PROGRESS: 'in_progress', + QUEUED: 'queued', + REQUESTED: 'requested', + WAITING: 'waiting', + PENDING: 'pending', + COMPLETED: 'completed', + FAILED: 'failed', + CANCELLED: 'cancelled', + SKIPPED: 'skipped', + TIMED_OUT: 'timed_out', +} as const; + +export const WORKFLOW_ACTIVE_STATUSES: readonly string[] = [ + WORKFLOW_STATUS.IN_PROGRESS, + WORKFLOW_STATUS.QUEUED, + WORKFLOW_STATUS.REQUESTED, + WORKFLOW_STATUS.WAITING, + WORKFLOW_STATUS.PENDING, +] as const; diff --git a/src/domain/__tests__/issue_inactivity.test.ts b/src/domain/__tests__/issue_inactivity.test.ts new file mode 100644 index 00000000..6fc7c690 --- /dev/null +++ b/src/domain/__tests__/issue_inactivity.test.ts @@ -0,0 +1,63 @@ +import { evaluateIssueInactivity } from '../issue_inactivity'; + +const waitingLabels = ['state:awaiting-maintainer', 'state:awaiting-issue-author']; + +function issue(overrides: Partial[0]['issue']> = {}) { + return { + number: 42, + updatedAt: '2026-08-28T00:00:00.000Z', + isPullRequest: false, + labels: [' State:Awaiting-Maintainer '], + ...overrides, + }; +} + +describe('issue inactivity policy', () => { + const nowMilliseconds = Date.parse('2026-09-04T00:00:00.000Z'); + + it('closes exactly at the configured threshold', () => { + expect(evaluateIssueInactivity({ + issue: issue(), + waitingLabels, + agentActivityLabel: 'state:ai-processing', + thresholdHours: 168, + nowMilliseconds, + })).toEqual({ + kind: 'close', + inactiveForMilliseconds: 168 * 60 * 60 * 1000, + }); + }); + + it.each([ + ['pull requests', issue({ isPullRequest: true }), 'pull-request'] as const, + ['issues without a waiting label', issue({ labels: ['bug'] }), 'not-waiting'] as const, + ['issues currently processed by an agent', issue({ labels: ['state:awaiting-maintainer', 'state:ai-processing'] }), 'agent-processing'] as const, + ['issues without a valid timestamp', issue({ updatedAt: undefined }), 'missing-activity-timestamp'] as const, + ['recently active issues', issue({ updatedAt: '2026-09-03T00:00:01.000Z' }), 'recent-activity'] as const, + ])('skips %s', (_description, candidate, reason) => { + expect(evaluateIssueInactivity({ + issue: candidate, + waitingLabels, + agentActivityLabel: 'state:ai-processing', + thresholdHours: 168, + nowMilliseconds, + })).toEqual({ kind: 'skip', reason }); + }); + + it('skips future timestamps and invalid thresholds', () => { + expect(evaluateIssueInactivity({ + issue: issue({ updatedAt: '2026-09-05T00:00:00.000Z' }), + waitingLabels, + agentActivityLabel: 'state:ai-processing', + thresholdHours: 168, + nowMilliseconds, + })).toEqual({ kind: 'skip', reason: 'future-activity' }); + expect(evaluateIssueInactivity({ + issue: issue(), + waitingLabels, + agentActivityLabel: 'state:ai-processing', + thresholdHours: 0, + nowMilliseconds, + })).toEqual({ kind: 'skip', reason: 'invalid-threshold' }); + }); +}); diff --git a/src/domain/issue_inactivity.ts b/src/domain/issue_inactivity.ts new file mode 100644 index 00000000..a0d93313 --- /dev/null +++ b/src/domain/issue_inactivity.ts @@ -0,0 +1,82 @@ +/** Default inactivity window used by the scheduled issue-maintenance action. */ +export const DEFAULT_INACTIVITY_THRESHOLD_HOURS = 168; + +/** Maximum supported window (one year) for a finite, operationally useful value. */ +export const MAX_INACTIVITY_THRESHOLD_HOURS = 8_760; + +export interface IssueActivitySnapshot { + readonly number: number; + readonly updatedAt?: string; + readonly isPullRequest: boolean; + readonly labels: readonly string[]; +} + +export type IssueInactivityDecision = + | { readonly kind: 'close'; readonly inactiveForMilliseconds: number } + | { + readonly kind: 'skip'; + readonly reason: + | 'pull-request' + | 'not-waiting' + | 'agent-processing' + | 'missing-activity-timestamp' + | 'future-activity' + | 'recent-activity' + | 'invalid-threshold'; + }; + +export interface IssueInactivityEvaluationInput { + readonly issue: IssueActivitySnapshot; + readonly waitingLabels: readonly string[]; + readonly agentActivityLabel: string; + readonly thresholdHours: number; + readonly nowMilliseconds: number; +} + +/** + * Decides whether an issue can be closed without depending on GitHub or time + * APIs. GitHub's `updated_at` is treated as the last activity observed by the + * provider; this includes comments and issue metadata changes. + */ +export function evaluateIssueInactivity( + input: IssueInactivityEvaluationInput, +): IssueInactivityDecision { + if (input.issue.isPullRequest) return { kind: 'skip', reason: 'pull-request' }; + if (!hasLabel(input.issue.labels, input.waitingLabels)) { + return { kind: 'skip', reason: 'not-waiting' }; + } + if (hasLabel(input.issue.labels, [input.agentActivityLabel])) { + return { kind: 'skip', reason: 'agent-processing' }; + } + if (!Number.isFinite(input.thresholdHours) + || input.thresholdHours <= 0 + || input.thresholdHours > MAX_INACTIVITY_THRESHOLD_HOURS) { + return { kind: 'skip', reason: 'invalid-threshold' }; + } + + const updatedAtMilliseconds = Date.parse(input.issue.updatedAt ?? ''); + if (!Number.isFinite(updatedAtMilliseconds)) { + return { kind: 'skip', reason: 'missing-activity-timestamp' }; + } + if (!Number.isFinite(input.nowMilliseconds) || updatedAtMilliseconds > input.nowMilliseconds) { + return { kind: 'skip', reason: 'future-activity' }; + } + + const inactiveForMilliseconds = input.nowMilliseconds - updatedAtMilliseconds; + const thresholdMilliseconds = input.thresholdHours * 60 * 60 * 1000; + return inactiveForMilliseconds >= thresholdMilliseconds + ? { kind: 'close', inactiveForMilliseconds } + : { kind: 'skip', reason: 'recent-activity' }; +} + +function hasLabel(labels: readonly string[], candidates: readonly string[]): boolean { + const normalizedLabels = new Set(labels.map(normalize)); + return candidates.some(candidate => { + const normalizedCandidate = normalize(candidate); + return normalizedCandidate.length > 0 && normalizedLabels.has(normalizedCandidate); + }); +} + +function normalize(value: string): string { + return value.trim().toLowerCase(); +} diff --git a/src/domain/setup.ts b/src/domain/setup.ts index f4ceea15..107dffb0 100644 --- a/src/domain/setup.ts +++ b/src/domain/setup.ts @@ -11,6 +11,7 @@ export type SetupFeature = | 'hotfix' | 'agentProvisioning' | 'credentialHealth' + | 'inactiveIssueClosure' | 'issueTemplates' | 'pullRequestTemplate'; @@ -41,6 +42,7 @@ export interface SetupRepositoryConfiguration { desiredAssigneesCount: number; desiredReviewersCount: number; mergeTimeout: number; + inactivityThresholdHours: number; issueLocale: string; pullRequestLocale: string; commitPrefixTransforms: string; diff --git a/src/infrastructure/composition/github_issue_client_factory.ts b/src/infrastructure/composition/github_issue_client_factory.ts index ba139fc2..a3396c6a 100644 --- a/src/infrastructure/composition/github_issue_client_factory.ts +++ b/src/infrastructure/composition/github_issue_client_factory.ts @@ -1,8 +1,9 @@ -import { OctokitIssueAssignmentClientAdapter, OctokitIssueContentClientAdapter, OctokitIssueLabelProvisioningClientAdapter, OctokitIssueLabelsClientAdapter, OctokitIssueLifecycleClientAdapter, OctokitIssueMetadataClientAdapter, OctokitIssueTitleClientAdapter } from "../github/octokit_issue_adapters"; +import { OctokitIssueAssignmentClientAdapter, OctokitIssueContentClientAdapter, OctokitIssueInactivityClientAdapter, OctokitIssueLabelProvisioningClientAdapter, OctokitIssueLabelsClientAdapter, OctokitIssueLifecycleClientAdapter, OctokitIssueMetadataClientAdapter, OctokitIssueTitleClientAdapter } from "../github/octokit_issue_adapters"; export const createIssueAssignmentClient = () => new OctokitIssueAssignmentClientAdapter(); export const createIssueContentClient = () => new OctokitIssueContentClientAdapter(); export const createIssueLabelProvisioningClient = () => new OctokitIssueLabelProvisioningClientAdapter(); export const createIssueLabelsClient = () => new OctokitIssueLabelsClientAdapter(); export const createIssueLifecycleClient = () => new OctokitIssueLifecycleClientAdapter(); +export const createIssueInactivityClient = () => new OctokitIssueInactivityClientAdapter(); export const createIssueMetadataClient = () => new OctokitIssueMetadataClientAdapter(); export const createIssueTitleClient = () => new OctokitIssueTitleClientAdapter(); diff --git a/src/infrastructure/composition/issue_inactivity_composition_root.ts b/src/infrastructure/composition/issue_inactivity_composition_root.ts new file mode 100644 index 00000000..3979af65 --- /dev/null +++ b/src/infrastructure/composition/issue_inactivity_composition_root.ts @@ -0,0 +1,13 @@ +import { CloseInactiveIssuesUseCase } from '../../application/usecases/actions/close_inactive_issues_use_case'; +import { IssueInactivityRepository } from '../../data/repository/issue/issue_inactivity_repository'; +import { SystemIssueInactivityClockAdapter } from '../time/system_issue_inactivity_clock_adapter'; +import { createIssueInactivityClient } from './github_issue_client_factory'; +import { createIssueClosureRepository } from './issue_interaction_composition_root'; + +export function createCloseInactiveIssuesUseCase(): CloseInactiveIssuesUseCase { + return new CloseInactiveIssuesUseCase( + new IssueInactivityRepository(createIssueInactivityClient()), + createIssueClosureRepository(), + new SystemIssueInactivityClockAdapter(), + ); +} diff --git a/src/infrastructure/composition/main_run_route_composition_root.ts b/src/infrastructure/composition/main_run_route_composition_root.ts index 74ceec6f..5a02cc87 100644 --- a/src/infrastructure/composition/main_run_route_composition_root.ts +++ b/src/infrastructure/composition/main_run_route_composition_root.ts @@ -52,6 +52,7 @@ import { createPullRequestUseCaseCompositionRoot } from "./pull_request_use_case import { createOrganizationMembersCompositionRoot } from "./organization_members_composition_root"; import { UpdatePullRequestDescriptionUseCase } from "../../application/usecases/steps/pull_request/update_pull_request_description_use_case"; import { PullRequestLifecycleRepository } from "../../data/repository/pull_request/pull_request_lifecycle_repository"; +import { createCloseInactiveIssuesUseCase } from "./issue_inactivity_composition_root"; function createDetectPotentialProblemsUseCase(): DetectPotentialProblemsUseCase { const bugbot = createBugbotCompositionRoot(); @@ -90,6 +91,7 @@ export function createSingleActionUseCaseCompositionRoot(): SingleActionUseCase issueDescriptionQueryPort, createFindingsQueryPort(), ), + createCloseInactiveIssuesUseCase(), ); } diff --git a/src/infrastructure/github/octokit_issue_adapters.ts b/src/infrastructure/github/octokit_issue_adapters.ts index 95c0ebdf..3f373379 100644 --- a/src/infrastructure/github/octokit_issue_adapters.ts +++ b/src/infrastructure/github/octokit_issue_adapters.ts @@ -1,6 +1,6 @@ import { getOctokitClient } from "./octokit_client_resolver"; import type { GithubClientPort } from "./ports/github_client_provider_port"; -import type { GithubIssueAssignmentClient, GithubIssueContentClient, GithubIssueLabelsClient, GithubIssueLifecycleClient, GithubIssueMetadataClient, GithubIssueTitleClient } from "./ports/github_issue_provider_ports"; +import type { GithubIssueAssignmentClient, GithubIssueContentClient, GithubIssueInactivityClient, GithubIssueLabelsClient, GithubIssueLifecycleClient, GithubIssueMetadataClient, GithubIssueTitleClient } from "./ports/github_issue_provider_ports"; import type { GithubIssueLabelProvisioningClient } from "./ports/github_issue_label_provisioning_protocol"; export class OctokitIssueAssignmentClientAdapter implements GithubClientPort { @@ -18,6 +18,9 @@ export class OctokitIssueLabelsClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueLifecycleClient { return getOctokitClient(token); } } +export class OctokitIssueInactivityClientAdapter implements GithubClientPort { + getClient(token: string): GithubIssueInactivityClient { return getOctokitClient(token); } +} export class OctokitIssueMetadataClientAdapter implements GithubClientPort { getClient(token: string): GithubIssueMetadataClient { return getOctokitClient(token); } } diff --git a/src/infrastructure/github/ports/github_issue_provider_ports.ts b/src/infrastructure/github/ports/github_issue_provider_ports.ts index 47ccb584..5a60ac16 100644 --- a/src/infrastructure/github/ports/github_issue_provider_ports.ts +++ b/src/infrastructure/github/ports/github_issue_provider_ports.ts @@ -7,6 +7,29 @@ export interface GithubIssueLifecycleClient { }; } +export interface GithubIssueInactivityClient { + paginate: { + iterator( + method: (parameters: Record) => Promise<{ data: GithubIssueActivity[] }>, + parameters: Record, + ): AsyncIterable<{ data: GithubIssueActivity[] }>; + }; + rest: { + issues: { + listForRepo(parameters: Record): Promise<{ data: GithubIssueActivity[] }>; + get(parameters: Record): Promise<{ data: GithubIssueActivity }>; + }; + }; +} + +export interface GithubIssueActivity { + number: number; + updated_at?: string | null; + state?: 'open' | 'closed' | string; + pull_request?: unknown; + labels?: Array<{ name?: string } | string>; +} + export interface GithubIssueContentClient { paginate: { iterator( diff --git a/src/infrastructure/time/__tests__/system_issue_inactivity_clock_adapter.test.ts b/src/infrastructure/time/__tests__/system_issue_inactivity_clock_adapter.test.ts new file mode 100644 index 00000000..a16ff230 --- /dev/null +++ b/src/infrastructure/time/__tests__/system_issue_inactivity_clock_adapter.test.ts @@ -0,0 +1,11 @@ +import { SystemIssueInactivityClockAdapter } from '../system_issue_inactivity_clock_adapter'; + +describe('SystemIssueInactivityClockAdapter', () => { + it('returns the current epoch time', () => { + const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(123_456); + + expect(new SystemIssueInactivityClockAdapter().nowMilliseconds()).toBe(123_456); + + nowSpy.mockRestore(); + }); +}); diff --git a/src/infrastructure/time/system_issue_inactivity_clock_adapter.ts b/src/infrastructure/time/system_issue_inactivity_clock_adapter.ts new file mode 100644 index 00000000..5c10308b --- /dev/null +++ b/src/infrastructure/time/system_issue_inactivity_clock_adapter.ts @@ -0,0 +1,7 @@ +import type { IssueInactivityClockPort } from '../../application/ports/issue_inactivity_ports'; + +export class SystemIssueInactivityClockAdapter implements IssueInactivityClockPort { + nowMilliseconds(): number { + return Date.now(); + } +} diff --git a/src/utils/setup_files.ts b/src/utils/setup_files.ts index 281dd099..f71c12ad 100644 --- a/src/utils/setup_files.ts +++ b/src/utils/setup_files.ts @@ -54,6 +54,7 @@ export function copySetupFiles( 'hotfix_workflow.yml': 'hotfix', 'agent-cli-provisioning.yml': 'agentProvisioning', 'copilot_credential_health.yml': 'credentialHealth', + 'copilot_close_inactive_issues.yml': 'inactiveIssueClosure', }; const approvedWorkflowFiles = new Set(options.approvedWorkflowFiles ?? []); const backupDirectory = options.updateExistingWorkflows ? path.join(cwd, '.copilot', 'setup-backups', new Date().toISOString().replace(/[:.]/g, '-')) : undefined; @@ -109,6 +110,7 @@ export function compareSetupWorkflows( 'hotfix_workflow.yml': 'hotfix', 'agent-cli-provisioning.yml': 'agentProvisioning', 'copilot_credential_health.yml': 'credentialHealth', + 'copilot_close_inactive_issues.yml': 'inactiveIssueClosure', }; const sourceDirectory = path.join(setupDir, 'workflows'); if (!fs.existsSync(sourceDirectory)) return []; From 7a0dd7f89925297c304769de0f2d892f70826172 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 4 Sep 2026 21:17:48 +0200 Subject: [PATCH 10/11] develop: align onboarding with pnpm setup --- README.md | 56 +++++++++++---- build/cli/index.js | 46 ++++++++----- .../infrastructure/cli/copilot_package.d.ts | 1 + .../cli/pnpm_cli_upgrade_adapter.d.ts | 6 ++ .../infrastructure/cli/copilot_package.d.ts | 1 + .../cli/pnpm_cli_upgrade_adapter.d.ts | 6 ++ docs/development/release-process.mdx | 2 +- docs/how-to-use.mdx | 68 ++++++++++++++----- docs/quick-start.mdx | 29 ++++++-- .../operations/upgrade-rollback.mdx | 2 +- docs/single-actions/workflow-and-cli.mdx | 44 +++++++++--- src/cli/commands/__tests__/upgrade.test.ts | 6 +- ...st.ts => pnpm_cli_upgrade_adapter.test.ts} | 36 +++++----- src/infrastructure/cli/copilot_package.ts | 1 + .../cli/npm_cli_update_check_adapter.ts | 2 +- ...adapter.ts => pnpm_cli_upgrade_adapter.ts} | 19 +++--- .../cli_upgrade_composition_root.ts | 4 +- 17 files changed, 234 insertions(+), 95 deletions(-) create mode 100644 build/cli/src/infrastructure/cli/copilot_package.d.ts create mode 100644 build/cli/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts create mode 100644 build/github_action/src/infrastructure/cli/copilot_package.d.ts create mode 100644 build/github_action/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts rename src/infrastructure/cli/__tests__/{npm_cli_upgrade_adapter.test.ts => pnpm_cli_upgrade_adapter.test.ts} (51%) create mode 100644 src/infrastructure/cli/copilot_package.ts rename src/infrastructure/cli/{npm_cli_upgrade_adapter.ts => pnpm_cli_upgrade_adapter.ts} (59%) diff --git a/README.md b/README.md index 55ab3dfb..853f778b 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,48 @@ Full documentation: **[docs.page/vypdev/copilot](https://docs.page/vypdev/copilo ## Getting started -1. **Create the workflow PAT** for the bot account and store it as a repository or organization Secret (e.g. `PAT`). `copilot setup` separately asks the operator for a setup PAT that is used only during local configuration, and lets you choose repository or organization scope independently for Secrets and Variables. See [Authentication](https://docs.page/vypdev/copilot/authentication). -2. **Use the action** from the marketplace so versions are stable: - ```yaml - uses: vypdev/copilot@v3 - ``` -3. **Install the CLI** when you want to run setup or single actions locally: - ```bash - npm install --global @vypdev/copilot - copilot --version - ``` - Update the published CLI later with **`copilot upgrade`**. Normal CLI commands may show a non-blocking notice when a newer release is available. -4. **Add workflows** — Copy the files from `setup/workflows/` into your `.github/workflows/`, or run **`copilot setup`** from your repo root. The setup wizard securely prompts for its separate operator PAT and can validate/provision the workflow PAT and provider credentials. See [How to use](https://docs.page/vypdev/copilot/how-to-use). +The recommended onboarding path is to install the published package globally with +`pnpm` and initialize the target repository with `copilot setup`: + +```bash +pnpm add --global @vypdev/copilot +copilot --version +cd /path/to/your/repository +copilot setup +``` + +`@vypdev/copilot` contains both the `copilot` CLI and the compiled GitHub Action. +The global installation makes the CLI and the setup templates available; it does +not install an Action into GitHub. `copilot setup` is the canonical initialization +flow and will, according to the selected features: + +- copy the required workflows, issue templates, and pull request template into the repository; +- create the labels and issue types used by the workflows; +- configure non-sensitive Repository Variables; and +- validate or provision the workflow PAT and provider credentials at the selected scope. + +The setup PAT entered by the operator is separate from the workflow `PAT` Secret. +Use `copilot setup --dry-run` to inspect the plan before making local or remote +changes. See the complete [How to use](https://docs.page/vypdev/copilot/how-to-use) +guide and [Authentication](https://docs.page/vypdev/copilot/authentication). + +### Manual workflow integration (advanced) + +You can integrate the Action manually when the CLI setup flow is not suitable: +copy selected files from `setup/workflows/` into `.github/workflows/` and add +steps such as: + +```yaml +- uses: vypdev/copilot@v3 + with: + token: ${{ secrets.PAT }} +``` + +This is a lower-level integration path, not an alternative name for +`copilot setup`: it does not automatically create labels, issue types, Variables, +Secrets, templates, or the complete set of workflows. Those resources must be +configured and kept consistent manually. See [Workflow setup](https://docs.page/vypdev/copilot/issues/workflow-setup) +for action-level examples. --- @@ -53,7 +83,7 @@ Full documentation: **[docs.page/vypdev/copilot](https://docs.page/vypdev/copilo - **Projects** — Link issues and PRs to boards and move them to the right columns. - **Single actions** — On-demand: check progress, think, create release/tag, mark deployed, etc. - **Evidence and safety** — Every run writes a bounded Job Summary; PR reviews expose a `Copilot / Review` Check Run, active findings fail that check, and all agent/comment content remains bounded and treated as untrusted data. -- **Concurrency** — Uses a repository-wide application queue across the seven Copilot/Task mutation workflows. Polling is adaptive and rate-limit-aware, with a 90-minute queue deadline and no cancellation or overwrite of intermediate runs. See [Features → Workflow concurrency](https://docs.page/vypdev/copilot/features#workflow-concurrency-and-sequential-execution). +- **Concurrency** — Uses a repository-wide application queue across the eight Copilot/Task mutation workflows. Polling is adaptive and rate-limit-aware, with a 90-minute queue deadline and no cancellation or overwrite of intermediate runs. See [Features → Workflow concurrency](https://docs.page/vypdev/copilot/features#workflow-concurrency-and-sequential-execution). AI features use the configured agent runtime and qualified model; see the [Agents](https://docs.page/vypdev/copilot/agents) and [Security & Operations](https://docs.page/vypdev/copilot/security-operations) documentation. You can run progress and Bugbot locally through the [Single actions → Workflow & CLI](https://docs.page/vypdev/copilot/single-actions/workflow-and-cli) path. diff --git a/build/cli/index.js b/build/cli/index.js index 6f8c3ccb..6c909233 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -76379,6 +76379,18 @@ function normalizeOrigin(origin) { } +/***/ }), + +/***/ 76182: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.COPILOT_PACKAGE_NAME = void 0; +exports.COPILOT_PACKAGE_NAME = '@vypdev/copilot'; + + /***/ }), /***/ 62007: @@ -76392,8 +76404,8 @@ exports.resolveUpdateCheckCachePath = resolveUpdateCheckCachePath; const node_fs_1 = __nccwpck_require__(87561); const node_os_1 = __nccwpck_require__(70612); const node_path_1 = __nccwpck_require__(49411); -const npm_cli_upgrade_adapter_1 = __nccwpck_require__(97258); -exports.NPM_REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(npm_cli_upgrade_adapter_1.COPILOT_PACKAGE_NAME)}`; +const copilot_package_1 = __nccwpck_require__(76182); +exports.NPM_REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(copilot_package_1.COPILOT_PACKAGE_NAME)}`; exports.UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1000; exports.UPDATE_CHECK_TIMEOUT_MS = 1500; function resolveUpdateCheckCachePath(platform = process.platform, environment = process.env, homeDirectory = (0, node_os_1.homedir)()) { @@ -76495,24 +76507,24 @@ exports.NpmCliUpdateCheckAdapter = NpmCliUpdateCheckAdapter; /***/ }), -/***/ 97258: +/***/ 64975: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.NpmCliUpgradeAdapter = exports.COPILOT_PACKAGE_NAME = void 0; -exports.resolveNpmExecutable = resolveNpmExecutable; +exports.PnpmCliUpgradeAdapter = void 0; +exports.resolvePnpmExecutable = resolvePnpmExecutable; const node_child_process_1 = __nccwpck_require__(17718); -exports.COPILOT_PACKAGE_NAME = '@vypdev/copilot'; -function resolveNpmExecutable(platform = process.platform) { - return platform === 'win32' ? 'npm.cmd' : 'npm'; +const copilot_package_1 = __nccwpck_require__(76182); +function resolvePnpmExecutable(platform = process.platform) { + return platform === 'win32' ? 'pnpm.cmd' : 'pnpm'; } -/** Executes the npm installation without invoking a shell or interpolating user input. */ -class NpmCliUpgradeAdapter { +/** Executes the pnpm installation without invoking a shell or interpolating user input. */ +class PnpmCliUpgradeAdapter { upgrade() { - const executable = resolveNpmExecutable(); - const args = ['install', '--global', `${exports.COPILOT_PACKAGE_NAME}@latest`]; + const executable = resolvePnpmExecutable(); + const args = ['add', '--global', `${copilot_package_1.COPILOT_PACKAGE_NAME}@latest`]; return new Promise((resolve, reject) => { const child = (0, node_child_process_1.spawn)(executable, args, { shell: false, @@ -76526,7 +76538,7 @@ class NpmCliUpgradeAdapter { reject(error); }; child.once('error', (error) => { - fail(new Error(`Unable to start npm upgrade: ${error.message}`)); + fail(new Error(`Unable to start pnpm upgrade: ${error.message}`)); }); child.once('close', (code, signal) => { if (settled) @@ -76537,12 +76549,12 @@ class NpmCliUpgradeAdapter { return; } const status = signal ? `signal ${signal}` : `exit code ${code ?? 'unknown'}`; - reject(new Error(`npm upgrade failed with ${status}.`)); + reject(new Error(`pnpm upgrade failed with ${status}.`)); }); }); } } -exports.NpmCliUpgradeAdapter = NpmCliUpgradeAdapter; +exports.PnpmCliUpgradeAdapter = PnpmCliUpgradeAdapter; /***/ }), @@ -76714,8 +76726,8 @@ function createCliUpdateCheckUseCase() { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createUpgradeCliUseCase = createUpgradeCliUseCase; const upgrade_cli_use_case_1 = __nccwpck_require__(45762); -const npm_cli_upgrade_adapter_1 = __nccwpck_require__(97258); -function createUpgradeCliUseCase(cliUpgradePort = new npm_cli_upgrade_adapter_1.NpmCliUpgradeAdapter()) { +const pnpm_cli_upgrade_adapter_1 = __nccwpck_require__(64975); +function createUpgradeCliUseCase(cliUpgradePort = new pnpm_cli_upgrade_adapter_1.PnpmCliUpgradeAdapter()) { return new upgrade_cli_use_case_1.UpgradeCliUseCase(cliUpgradePort); } diff --git a/build/cli/src/infrastructure/cli/copilot_package.d.ts b/build/cli/src/infrastructure/cli/copilot_package.d.ts new file mode 100644 index 00000000..6c7f1c0c --- /dev/null +++ b/build/cli/src/infrastructure/cli/copilot_package.d.ts @@ -0,0 +1 @@ +export declare const COPILOT_PACKAGE_NAME = "@vypdev/copilot"; diff --git a/build/cli/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts b/build/cli/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts new file mode 100644 index 00000000..55045f31 --- /dev/null +++ b/build/cli/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts @@ -0,0 +1,6 @@ +import type { CliUpgradePort } from '../../application/ports/cli_upgrade_ports'; +export declare function resolvePnpmExecutable(platform?: NodeJS.Platform): string; +/** Executes the pnpm installation without invoking a shell or interpolating user input. */ +export declare class PnpmCliUpgradeAdapter implements CliUpgradePort { + upgrade(): Promise; +} diff --git a/build/github_action/src/infrastructure/cli/copilot_package.d.ts b/build/github_action/src/infrastructure/cli/copilot_package.d.ts new file mode 100644 index 00000000..6c7f1c0c --- /dev/null +++ b/build/github_action/src/infrastructure/cli/copilot_package.d.ts @@ -0,0 +1 @@ +export declare const COPILOT_PACKAGE_NAME = "@vypdev/copilot"; diff --git a/build/github_action/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts b/build/github_action/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts new file mode 100644 index 00000000..55045f31 --- /dev/null +++ b/build/github_action/src/infrastructure/cli/pnpm_cli_upgrade_adapter.d.ts @@ -0,0 +1,6 @@ +import type { CliUpgradePort } from '../../application/ports/cli_upgrade_ports'; +export declare function resolvePnpmExecutable(platform?: NodeJS.Platform): string; +/** Executes the pnpm installation without invoking a shell or interpolating user input. */ +export declare class PnpmCliUpgradeAdapter implements CliUpgradePort { + upgrade(): Promise; +} diff --git a/docs/development/release-process.mdx b/docs/development/release-process.mdx index 0de6fee8..3c284314 100644 --- a/docs/development/release-process.mdx +++ b/docs/development/release-process.mdx @@ -42,7 +42,7 @@ To verify a published version independently: ```bash npm view @vypdev/copilot version -npm install --global @vypdev/copilot@ +pnpm add --global @vypdev/copilot@ copilot --version ``` diff --git a/docs/how-to-use.mdx b/docs/how-to-use.mdx index a382a749..3cdf5cbc 100644 --- a/docs/how-to-use.mdx +++ b/docs/how-to-use.mdx @@ -3,12 +3,14 @@ title: How To Use description: Quick setup with copilot setup, then customize labels, templates, and workflows. --- -## Quick installation (CLI) +## Recommended installation and initialization -The recommended installation is the public npm package. The package is scoped as `@vypdev/copilot`, but it exposes the global `copilot` executable: +The recommended path is to install the published package globally with `pnpm` and +then initialize the target repository with `copilot setup`. The package is scoped +as `@vypdev/copilot`, but it exposes the global `copilot` executable: ```bash -npm install --global @vypdev/copilot +pnpm add --global @vypdev/copilot copilot --version ``` @@ -34,11 +36,13 @@ If the checkout does not include the compiled `build/` folder (e.g. it is gitign Once installed, the `copilot` command is available globally. Repository-dependent commands such as `copilot setup`, `copilot doctor`, `copilot check-progress`, `copilot think`, and `copilot do` must be run **from the root of the target repository**. The `copilot upgrade`, `copilot --version`, and help flows can run from any directory. Commands that access GitHub accept `--token` or `PERSONAL_ACCESS_TOKEN` from the environment. `copilot setup` and `copilot doctor` securely prompt for the setup PAT when run interactively; no `.env` file is read or created. `copilot setup --dry-run` is the only setup mode that can run without a token. See [CLI commands](/single-actions/workflow-and-cli). -If you previously installed Copilot from a local checkout, installing the npm package switches the same `copilot` command to the published package. Check which executable and package are active: +If you previously installed Copilot from a local checkout, installing the published +package with pnpm switches the same `copilot` command to the published package. +Check which executable and package are active: ```bash command -v copilot -npm list --global --depth 0 @vypdev/copilot +pnpm list --global --depth 0 @vypdev/copilot copilot --version ``` @@ -54,14 +58,19 @@ corepack pnpm remove --global copilot ### Update or reinstall the global CLI -For a published release, update the global npm package: +For a published release, update the global pnpm package: ```bash copilot upgrade copilot --version ``` -`copilot upgrade` can be run from any directory; it updates the published npm installation and does not require a GitHub repository or `PERSONAL_ACCESS_TOKEN`. If the installed version does not include the `upgrade` command, bootstrap it once with `npm install --global @vypdev/copilot@latest`, then use `copilot upgrade` for future updates. +`copilot upgrade` can be run from any directory; it updates the published pnpm +installation and does not require a GitHub repository or `PERSONAL_ACCESS_TOKEN`. +If the installed version does not include the `upgrade` command, bootstrap it once +with `pnpm add --global @vypdev/copilot@latest`, then use `copilot upgrade` for +future updates. Running `pnpm update --global @vypdev/copilot` directly is also +supported. Before running a normal CLI command, Copilot performs a lightweight, informational check for a newer published version. The check uses a local 24-hour cache, has a short timeout, never blocks or changes the command result, and stays silent when npm is unavailable. If an update is found, Copilot prints `A new version (x.y.z) is available. Run "copilot upgrade".` The `upgrade`, `--version`, and help flows do not trigger this check. To disable it, set `COPILOT_DISABLE_UPDATE_CHECK=1`. @@ -73,18 +82,12 @@ git pull --ff-only origin master corepack pnpm install . --global --force ``` -After an npm installation, confirm the executable and installed package: +After installation, confirm the executable and installed package: ```bash command -v copilot copilot --help copilot --version -npm list --global --depth 0 @vypdev/copilot -``` - -If you installed from a checkout with pnpm, use this package listing instead: - -```bash corepack pnpm list --global --depth 0 @vypdev/copilot ``` @@ -92,7 +95,7 @@ The complete command reference, including every supported option, is in [Workflo --- -## Tutorial: Get Copilot running in three steps +## Tutorial: Initialize a repository @@ -152,9 +155,42 @@ The complete command reference, including every supported option, is in [Workflo --- +## Manual workflow integration (advanced) + +The Action can also be wired manually from a GitHub Actions workflow. This is a +lower-level integration path for repositories that cannot run the setup wizard; +it is not equivalent to the recommended `copilot setup` flow. + +1. Copy only the workflow files you need from the installed package's `setup/workflows/` + directory into `.github/workflows/`. +2. Ensure every Copilot step uses a stable Action reference such as + `vypdev/copilot@v3` and passes a write-capable `token` where required. +3. Configure the labels, issue types, Variables, Secrets, templates, permissions, + and workflow dependencies yourself. + +For example: + +```yaml +- uses: vypdev/copilot@v3 + with: + token: ${{ secrets.PAT }} +``` + +Manual workflow installation only adds execution wiring. It does not initialize +the repository metadata or install the complete set of Copilot workflows. Prefer +`copilot setup` unless this manual control is intentional. See [Workflow setup](/issues/workflow-setup) +for the action-level contract. + +--- + ## What lives in `setup/` (and what gets created) -The workflow and template files under `setup/` are copied into your repo by `copilot setup`. Credentials are never shipped in the package or written to local configuration files. Below is a breakdown of labels (with defaults), issue types, and the contents of `setup/` so you know what you can customize and what must stay consistent. +The workflow and template files under `setup/` are copied into your repo by +`copilot setup`. Manual copying is supported as an advanced integration path, but +it does not perform the rest of initialization. Credentials are never shipped in +the package or written to local configuration files. Below is a breakdown of +labels (with defaults), issue types, and the contents of `setup/` so you know what +you can customize and what must stay consistent. ### Coherence when customizing diff --git a/docs/quick-start.mdx b/docs/quick-start.mdx index b7039b83..be519cb6 100644 --- a/docs/quick-start.mdx +++ b/docs/quick-start.mdx @@ -4,11 +4,29 @@ description: Minimal verified setup for the Copilot Action. --- # Quick start -## Prerequisites +## Recommended setup -You MUST have a GitHub repository, a fine-grained token stored as a repository secret, and a workflow that checks out the repository before invoking Copilot. Agent features additionally require either a verified CLI already installed on the runner or a pinned provisioning input. Codex may use the runner's preinitialized session instead of an exported credential. +The supported onboarding path is the global CLI package followed by +`copilot setup`. It initializes the repository and lets you select the workflows, +templates, labels, issue types, Variables, Secrets, and agent settings together. -## Minimal workflow +```bash +pnpm add --global @vypdev/copilot +copilot --version +cd /path/to/your/repository +copilot setup +``` + +Use `copilot setup --dry-run` to review the plan without changing files or GitHub. +The setup PAT used by the operator is separate from the workflow `PAT` Secret. +After setup, review and commit the generated `.github/` files, then customize the +selected workflows as needed. See [How to use](/how-to-use) for the full flow. + +## Manual workflow integration (advanced) + +If the setup wizard cannot be used, the Action can be wired directly into a +workflow. This only configures execution; it does not initialize repository +metadata or install the complete Copilot workflow set. ```yaml name: Copilot @@ -33,7 +51,10 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ``` -The example uses the stable `v3` major ref. For strict reproducibility, replace it with an immutable release tag after selecting the version you have validated. +Configure labels, issue types, Variables, Secrets, templates, permissions, and +additional workflows manually when following this path. The example uses the +stable `v3` major ref; for strict reproducibility, replace it with an immutable +release tag after selecting the version you have validated. ## Verify before enabling writes diff --git a/docs/security-operations/operations/upgrade-rollback.mdx b/docs/security-operations/operations/upgrade-rollback.mdx index 546dd29b..b0f1c1da 100644 --- a/docs/security-operations/operations/upgrade-rollback.mdx +++ b/docs/security-operations/operations/upgrade-rollback.mdx @@ -23,7 +23,7 @@ For a controlled rollout or rollback, install a specific package version explici ```bash VERSION= -npm install --global "@vypdev/copilot@${VERSION}" +pnpm add --global "@vypdev/copilot@${VERSION}" copilot --version ``` diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index 0c178d14..204a01d9 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -7,7 +7,24 @@ description: Run single actions from GitHub Actions workflows and from the Copil You can run single actions in two ways: from a **GitHub Actions workflow** (for example, a scheduled job or manual trigger) or from the **`copilot` CLI** locally. -## From a GitHub Actions workflow +## Recommended: initialize with the CLI + +For a new repository, install the published package globally with `pnpm` and let +`copilot setup` initialize the repository. This is the supported onboarding path: + +```bash +pnpm add --global @vypdev/copilot +copilot --version +cd /path/to/your/repository +copilot setup +``` + +The wizard copies the selected workflows and templates, creates the required +labels and issue types, and configures the Variables and Secrets required by the +selected features. Use `copilot setup --dry-run` to review the plan first. See +[How to use](/how-to-use) for the setup PAT, workflow PAT, and scope details. + +## Manual: run the Action from a GitHub Actions workflow Add a job that sets `single-action` and any required inputs (`single-action-issue`, `single-action-version`, etc.): @@ -28,16 +45,19 @@ Use `workflow_dispatch` to run on demand, or trigger the workflow from another e ## Install or update the CLI -The published package is `@vypdev/copilot` and exposes one global executable: **`copilot`**. Install it from npm, then run repository-dependent commands from the target repository. +The published package is `@vypdev/copilot` and exposes one global executable: +**`copilot`**. Install it with pnpm, then run repository-dependent commands from +the target repository. ### Install the published package ```bash -npm install --global @vypdev/copilot +pnpm add --global @vypdev/copilot copilot --version ``` -The package requires Node.js 24 or newer. The npm scope does not change the executable name: the command remains `copilot`. +The package requires Node.js 24 or newer. The npm scope does not change the +executable name: the command remains `copilot`. ### Install from a checkout @@ -57,7 +77,7 @@ corepack pnpm run build corepack pnpm install . --global --force ``` -### Update an npm installation +### Update the global pnpm installation Update to the latest published version with: @@ -66,7 +86,11 @@ copilot upgrade copilot --version ``` -Run `copilot upgrade` from any directory. It updates the published npm installation and does not require a target repository or GitHub token. If the installed version does not include this command, run `npm install --global @vypdev/copilot@latest` once and then use `copilot upgrade`. +Run `copilot upgrade` from any directory. It updates the published pnpm +installation and does not require a target repository or GitHub token. If the +installed version does not include this command, run +`pnpm add --global @vypdev/copilot@latest` once. You can also update it directly +with `pnpm update --global @vypdev/copilot`. Normal Copilot CLI commands also perform a lightweight update check against npm. It is informational only: the result is cached locally for 24 hours, the network request has a short timeout, and failures are ignored so the command continues normally. When a newer release is found, the CLI prints `A new version (x.y.z) is available. Run "copilot upgrade".` The check is skipped for `copilot upgrade`, `--version`, and help. Set `COPILOT_DISABLE_UPDATE_CHECK=1` to opt out. @@ -75,7 +99,7 @@ Verify the installed package and executable: ```bash command -v copilot copilot --version -npm list --global --depth 0 @vypdev/copilot +pnpm list --global --depth 0 @vypdev/copilot ``` ### Reinstall or update from a checkout @@ -97,9 +121,11 @@ copilot --version corepack pnpm list --global --depth 0 @vypdev/copilot ``` -If the command is not found after installation, configure the npm or pnpm global bin directory in your shell's `PATH` and open a new shell. +If the command is not found after installation, configure pnpm's global bin +directory in your shell's `PATH` and open a new shell. -For an npm installation, `npm prefix --global` shows the npm global prefix; on macOS and Linux, the executable directory is normally its `bin` subdirectory. For a checkout installation, use the equivalent global bin directory reported by pnpm. +Run `pnpm bin --global` or consult `pnpm setup` to locate the global executable +directory when configuring `PATH`. ## Local CLI prerequisites diff --git a/src/cli/commands/__tests__/upgrade.test.ts b/src/cli/commands/__tests__/upgrade.test.ts index 05de27fd..a52db0d2 100644 --- a/src/cli/commands/__tests__/upgrade.test.ts +++ b/src/cli/commands/__tests__/upgrade.test.ts @@ -2,7 +2,7 @@ import { Command } from 'commander'; import { registerUpgradeCommand, runUpgradeCommand } from '../upgrade'; describe('upgrade command adapter', () => { - it('registers the upgrade command with npm-specific help', () => { + it('registers the upgrade command with package-specific help', () => { const program = new Command(); registerUpgradeCommand(program); @@ -24,7 +24,7 @@ describe('upgrade command adapter', () => { }); it('sets a failure exit code when the upgrade fails', async () => { - const runner = { execute: jest.fn().mockRejectedValue(new Error('npm failed')) }; + const runner = { execute: jest.fn().mockRejectedValue(new Error('pnpm failed')) }; const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const previousExitCode = process.exitCode; process.exitCode = undefined; @@ -32,7 +32,7 @@ describe('upgrade command adapter', () => { await runUpgradeCommand(runner); expect(process.exitCode).toBe(1); - expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('npm failed')); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('pnpm failed')); process.exitCode = previousExitCode; errorSpy.mockRestore(); }); diff --git a/src/infrastructure/cli/__tests__/npm_cli_upgrade_adapter.test.ts b/src/infrastructure/cli/__tests__/pnpm_cli_upgrade_adapter.test.ts similarity index 51% rename from src/infrastructure/cli/__tests__/npm_cli_upgrade_adapter.test.ts rename to src/infrastructure/cli/__tests__/pnpm_cli_upgrade_adapter.test.ts index 97d42933..018c51a3 100644 --- a/src/infrastructure/cli/__tests__/npm_cli_upgrade_adapter.test.ts +++ b/src/infrastructure/cli/__tests__/pnpm_cli_upgrade_adapter.test.ts @@ -1,37 +1,37 @@ import { EventEmitter } from 'node:events'; import { spawn } from 'node:child_process'; import { - COPILOT_PACKAGE_NAME, - NpmCliUpgradeAdapter, - resolveNpmExecutable, -} from '../npm_cli_upgrade_adapter'; + PnpmCliUpgradeAdapter, + resolvePnpmExecutable, +} from '../pnpm_cli_upgrade_adapter'; +import { COPILOT_PACKAGE_NAME } from '../copilot_package'; jest.mock('node:child_process', () => ({ spawn: jest.fn(), })); -describe('NpmCliUpgradeAdapter', () => { +describe('PnpmCliUpgradeAdapter', () => { const spawnMock = spawn as jest.MockedFunction; beforeEach(() => { jest.clearAllMocks(); }); - it('resolves the platform-specific npm executable', () => { - expect(resolveNpmExecutable('darwin')).toBe('npm'); - expect(resolveNpmExecutable('linux')).toBe('npm'); - expect(resolveNpmExecutable('win32')).toBe('npm.cmd'); + it('resolves the platform-specific pnpm executable', () => { + expect(resolvePnpmExecutable('darwin')).toBe('pnpm'); + expect(resolvePnpmExecutable('linux')).toBe('pnpm'); + expect(resolvePnpmExecutable('win32')).toBe('pnpm.cmd'); }); it('installs the latest scoped package globally without a shell', async () => { const child = new EventEmitter(); spawnMock.mockReturnValue(child as ReturnType); - const upgrade = new NpmCliUpgradeAdapter().upgrade(); + const upgrade = new PnpmCliUpgradeAdapter().upgrade(); expect(spawnMock).toHaveBeenCalledWith( - resolveNpmExecutable(), - ['install', '--global', `${COPILOT_PACKAGE_NAME}@latest`], + resolvePnpmExecutable(), + ['add', '--global', `${COPILOT_PACKAGE_NAME}@latest`], { shell: false, stdio: 'inherit' }, ); child.emit('close', 0, null); @@ -39,23 +39,23 @@ describe('NpmCliUpgradeAdapter', () => { await expect(upgrade).resolves.toBeUndefined(); }); - it('reports a non-zero npm exit code', async () => { + it('reports a non-zero pnpm exit code', async () => { const child = new EventEmitter(); spawnMock.mockReturnValue(child as ReturnType); - const upgrade = new NpmCliUpgradeAdapter().upgrade(); + const upgrade = new PnpmCliUpgradeAdapter().upgrade(); child.emit('close', 1, null); await expect(upgrade).rejects.toThrow('exit code 1'); }); - it('reports an npm process startup failure', async () => { + it('reports a pnpm process startup failure', async () => { const child = new EventEmitter(); spawnMock.mockReturnValue(child as ReturnType); - const upgrade = new NpmCliUpgradeAdapter().upgrade(); - child.emit('error', new Error('npm not found')); + const upgrade = new PnpmCliUpgradeAdapter().upgrade(); + child.emit('error', new Error('pnpm not found')); - await expect(upgrade).rejects.toThrow('npm not found'); + await expect(upgrade).rejects.toThrow('pnpm not found'); }); }); diff --git a/src/infrastructure/cli/copilot_package.ts b/src/infrastructure/cli/copilot_package.ts new file mode 100644 index 00000000..143082ed --- /dev/null +++ b/src/infrastructure/cli/copilot_package.ts @@ -0,0 +1 @@ +export const COPILOT_PACKAGE_NAME = '@vypdev/copilot'; diff --git a/src/infrastructure/cli/npm_cli_update_check_adapter.ts b/src/infrastructure/cli/npm_cli_update_check_adapter.ts index ed2c7bd5..553e2b7a 100644 --- a/src/infrastructure/cli/npm_cli_update_check_adapter.ts +++ b/src/infrastructure/cli/npm_cli_update_check_adapter.ts @@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { dirname, join } from 'node:path'; import type { CliUpdateCheckPort } from '../../application/ports/cli_update_check_ports'; -import { COPILOT_PACKAGE_NAME } from './npm_cli_upgrade_adapter'; +import { COPILOT_PACKAGE_NAME } from './copilot_package'; export const NPM_REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(COPILOT_PACKAGE_NAME)}`; export const UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1000; diff --git a/src/infrastructure/cli/npm_cli_upgrade_adapter.ts b/src/infrastructure/cli/pnpm_cli_upgrade_adapter.ts similarity index 59% rename from src/infrastructure/cli/npm_cli_upgrade_adapter.ts rename to src/infrastructure/cli/pnpm_cli_upgrade_adapter.ts index 9da84133..f72d8931 100644 --- a/src/infrastructure/cli/npm_cli_upgrade_adapter.ts +++ b/src/infrastructure/cli/pnpm_cli_upgrade_adapter.ts @@ -1,17 +1,16 @@ import { spawn } from 'node:child_process'; import type { CliUpgradePort } from '../../application/ports/cli_upgrade_ports'; +import { COPILOT_PACKAGE_NAME } from './copilot_package'; -export const COPILOT_PACKAGE_NAME = '@vypdev/copilot'; - -export function resolveNpmExecutable(platform: NodeJS.Platform = process.platform): string { - return platform === 'win32' ? 'npm.cmd' : 'npm'; +export function resolvePnpmExecutable(platform: NodeJS.Platform = process.platform): string { + return platform === 'win32' ? 'pnpm.cmd' : 'pnpm'; } -/** Executes the npm installation without invoking a shell or interpolating user input. */ -export class NpmCliUpgradeAdapter implements CliUpgradePort { +/** Executes the pnpm installation without invoking a shell or interpolating user input. */ +export class PnpmCliUpgradeAdapter implements CliUpgradePort { upgrade(): Promise { - const executable = resolveNpmExecutable(); - const args = ['install', '--global', `${COPILOT_PACKAGE_NAME}@latest`]; + const executable = resolvePnpmExecutable(); + const args = ['add', '--global', `${COPILOT_PACKAGE_NAME}@latest`]; return new Promise((resolve, reject) => { const child = spawn(executable, args, { @@ -26,7 +25,7 @@ export class NpmCliUpgradeAdapter implements CliUpgradePort { }; child.once('error', (error) => { - fail(new Error(`Unable to start npm upgrade: ${error.message}`)); + fail(new Error(`Unable to start pnpm upgrade: ${error.message}`)); }); child.once('close', (code, signal) => { if (settled) return; @@ -36,7 +35,7 @@ export class NpmCliUpgradeAdapter implements CliUpgradePort { return; } const status = signal ? `signal ${signal}` : `exit code ${code ?? 'unknown'}`; - reject(new Error(`npm upgrade failed with ${status}.`)); + reject(new Error(`pnpm upgrade failed with ${status}.`)); }); }); } diff --git a/src/infrastructure/composition/cli_upgrade_composition_root.ts b/src/infrastructure/composition/cli_upgrade_composition_root.ts index 69316542..5822f7b7 100644 --- a/src/infrastructure/composition/cli_upgrade_composition_root.ts +++ b/src/infrastructure/composition/cli_upgrade_composition_root.ts @@ -1,9 +1,9 @@ import type { CliUpgradePort } from '../../application/ports/cli_upgrade_ports'; import { UpgradeCliUseCase } from '../../application/usecases/upgrade_cli_use_case'; -import { NpmCliUpgradeAdapter } from '../cli/npm_cli_upgrade_adapter'; +import { PnpmCliUpgradeAdapter } from '../cli/pnpm_cli_upgrade_adapter'; export function createUpgradeCliUseCase( - cliUpgradePort: CliUpgradePort = new NpmCliUpgradeAdapter(), + cliUpgradePort: CliUpgradePort = new PnpmCliUpgradeAdapter(), ): UpgradeCliUseCase { return new UpgradeCliUseCase(cliUpgradePort); } From 5e99c194a17e7a282756e2aea2498baa2f85f8da Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sat, 5 Sep 2026 23:58:30 +0200 Subject: [PATCH 11/11] develop: scope workflow queue and skip bot reruns --- build/cli/index.js | 52 ++++---- .../policies/workflow_queue_policy.d.ts | 7 +- .../application/ports/workflow_run_ports.d.ts | 3 - .../ports/github_workflow_provider_ports.d.ts | 5 +- build/github_action/index.js | 52 ++++---- .../policies/workflow_queue_policy.d.ts | 7 +- .../application/ports/workflow_run_ports.d.ts | 3 - .../ports/github_workflow_provider_ports.d.ts | 5 +- docs/development/architecture.mdx | 27 ++-- docs/features.mdx | 20 +-- src/actions/__tests__/common_action.test.ts | 24 ++-- src/actions/common_action.ts | 3 +- src/actions/main_run_lifecycle.ts | 13 +- .../policies/workflow_queue_policy.ts | 7 +- src/application/ports/workflow_run_ports.ts | 3 - ...or_previous_workflow_runs_use_case.test.ts | 4 +- ..._previous_workflow_runs_repository.test.ts | 119 +++++++----------- ...ctive_previous_workflow_runs_repository.ts | 31 ++--- .../workflow_queue_composition_root.test.ts | 14 +-- .../ports/github_workflow_provider_ports.ts | 5 +- 20 files changed, 167 insertions(+), 237 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 6c909233..672ff210 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -54562,6 +54562,7 @@ const agent_activity_policy_1 = __nccwpck_require__(15375); const main_run_lifecycle_1 = __nccwpck_require__(916); async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, lifecycleStateUseCase, agentActivityUseCase) { (0, logging_ports_1.configureApplicationLogger)((0, logger_adapter_1.createLoggerAdapter)()); + (0, logging_ports_1.setGlobalLoggerDebug)(execution.debug, execution.inputs === undefined); const repository = (0, repository_context_1.requireRepositoryCoordinates)({ owner: execution.owner, repo: execution.repo, @@ -55537,7 +55538,6 @@ const logger_1 = __nccwpck_require__(91151); const main_run_dispatcher_1 = __nccwpck_require__(28586); const workflow_context_1 = __nccwpck_require__(55224); const workflow_queue_composition_root_1 = __nccwpck_require__(21598); -const workflow_queue_policy_1 = __nccwpck_require__(43193); exports.WORKFLOW_QUEUE_FAILURE_MESSAGE = 'Workflow queue check failed; sequential execution was not bypassed.'; /** * Keeps provider diagnostics out of the action's externally visible failure @@ -55551,17 +55551,13 @@ class WorkflowQueueFailureError extends Error { } exports.WorkflowQueueFailureError = WorkflowQueueFailureError; function buildPreviousWorkflowRunsQuery(repository) { + const workflowIdentifier = (0, workflow_context_1.resolveWorkflowIdentifier)(process.env.GITHUB_WORKFLOW_REF); const query = { owner: repository.owner, repository: repository.repo, currentRunId: Number.parseInt(process.env.GITHUB_RUN_ID ?? '', 10), - workflowName: process.env.GITHUB_WORKFLOW ?? '', - workflowNames: workflow_queue_policy_1.COPILOT_WORKFLOW_NAMES, + ...(workflowIdentifier ? { workflowIdentifier } : {}), }; - const workflowIdentifier = (0, workflow_context_1.resolveWorkflowIdentifier)(process.env.GITHUB_WORKFLOW_REF); - if (workflowIdentifier) { - query.workflowIdentifier = workflowIdentifier; - } return query; } async function waitForPreviousWorkflowRuns(token, repository) { @@ -55569,6 +55565,9 @@ async function waitForPreviousWorkflowRuns(token, repository) { if (process.env.GITHUB_ACTIONS === 'true' && !Number.isSafeInteger(query.currentRunId)) { throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.'); } + if (process.env.GITHUB_ACTIONS === 'true' && !query.workflowIdentifier) { + throw new Error('GitHub workflow identifier is unavailable; refusing to bypass sequential execution.'); + } await (0, workflow_queue_composition_root_1.createWaitForPreviousWorkflowRunsUseCase)(token) .invoke(query) .catch(() => { @@ -57936,9 +57935,10 @@ exports.WORKFLOW_QUEUE_POLICY = exports.COPILOT_WORKFLOW_NAMES = void 0; exports.calculateWorkflowPollingDelay = calculateWorkflowPollingDelay; exports.calculateJitteredWorkflowDelay = calculateJitteredWorkflowDelay; /** - * Workflows that execute the Copilot action and therefore share its - * repository mutation queue. Keep these names aligned with workflow `name` - * values in `.github/workflows` and the setup templates. + * Workflows that execute the Copilot action. Keep these names aligned with + * workflow `name` values in `.github/workflows` and the setup templates. + * Queue admission is scoped to the current workflow file; this list is kept + * for workflow-contract validation. */ exports.COPILOT_WORKFLOW_NAMES = [ 'Copilot - Issue', @@ -75433,36 +75433,28 @@ class ActivePreviousWorkflowRunsRepository { if (!Number.isSafeInteger(query.currentRunId)) { throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.'); } - const workflowNames = query.workflowNames?.filter(name => name.trim().length > 0) ?? []; - if (workflowNames.length === 0 && query.workflowName.trim().length === 0) { - throw new Error('GitHub workflow name is unavailable; refusing to bypass sequential execution.'); + const workflowIdentifier = query.workflowIdentifier?.trim() ?? ''; + if (workflowIdentifier.length === 0) { + throw new Error('GitHub workflow identifier is unavailable; refusing to bypass sequential execution.'); } const actions = this.client.rest.actions; - const workflowIdentifier = workflowNames.length === 0 ? query.workflowIdentifier : undefined; - const workflowMethod = workflowIdentifier ? actions.listWorkflowRuns : undefined; - const method = workflowMethod ?? actions.listWorkflowRunsForRepo; + const method = actions.listWorkflowRuns; if (!method) throw new Error('GitHub workflow-scoped runs endpoint is unavailable.'); const parameters = { owner: query.owner, repo: query.repository, per_page: 100, - ...(workflowMethod && workflowIdentifier - ? { workflow_id: workflowIdentifier } - : {}), + workflow_id: workflowIdentifier, }; - const names = workflowNames.length > 0 ? workflowNames : [query.workflowName]; return (0, workflow_runs_retry_1.withWorkflowRunsRetry)(async () => { let activeRunCount = 0; - // Keep one complete sequential traversal: GitHub cannot safely express - // the eight shared workflow names, five active statuses, or the strict - // lower-ID predicate in this endpoint. Do not add provider filters or - // early-stop on page order; a matching run may occur on a later page. - // The residual cost is deep-history pagination, with retries restarting - // from page one, in exchange for an exact fail-closed count. + // The workflow-scoped endpoint limits the traversal to this workflow. + // Keep pagination exhaustive because an active older run may occur on + // a later page, while filtering status and run identity locally. for await (const response of this.client.paginate.iterator(method, parameters)) { activeRunCount += extractWorkflowRuns(response) - .filter(run => isActivePreviousRun(run, query, names)).length; + .filter(run => isActivePreviousRun(run, query)).length; } return activeRunCount; }, { @@ -75485,10 +75477,8 @@ function extractWorkflowRuns(response) { } throw new Error('GitHub workflow runs response did not contain a workflow_runs array.'); } -function isActivePreviousRun(run, query, workflowNames) { - return typeof run.name === 'string' - && workflowNames.includes(run.name) - && run.id < query.currentRunId +function isActivePreviousRun(run, query) { + return run.id < query.currentRunId && workflow_status_1.WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); } diff --git a/build/cli/src/application/policies/workflow_queue_policy.d.ts b/build/cli/src/application/policies/workflow_queue_policy.d.ts index 98937d72..eb2732d8 100644 --- a/build/cli/src/application/policies/workflow_queue_policy.d.ts +++ b/build/cli/src/application/policies/workflow_queue_policy.d.ts @@ -1,7 +1,8 @@ /** - * Workflows that execute the Copilot action and therefore share its - * repository mutation queue. Keep these names aligned with workflow `name` - * values in `.github/workflows` and the setup templates. + * Workflows that execute the Copilot action. Keep these names aligned with + * workflow `name` values in `.github/workflows` and the setup templates. + * Queue admission is scoped to the current workflow file; this list is kept + * for workflow-contract validation. */ export declare const COPILOT_WORKFLOW_NAMES: readonly ["Copilot - Issue", "Copilot - Issue Comment", "Copilot - Commit", "Copilot - Pull Request", "Copilot - Pull Request Comment", "Copilot - Close Inactive Issues", "Task - Hotfix", "Task - Release"]; export interface WorkflowPollingPolicy { diff --git a/build/cli/src/application/ports/workflow_run_ports.d.ts b/build/cli/src/application/ports/workflow_run_ports.d.ts index 5f8c26d4..7f60a54b 100644 --- a/build/cli/src/application/ports/workflow_run_ports.d.ts +++ b/build/cli/src/application/ports/workflow_run_ports.d.ts @@ -2,11 +2,8 @@ export interface PreviousWorkflowRunsQuery { owner: string; repository: string; currentRunId: number; - workflowName: string; /** Workflow file name accepted by GitHub to scope the queue query. */ workflowIdentifier?: string; - /** All Copilot workflow names share one queue so different event workflows cannot overlap. */ - workflowNames?: readonly string[]; } export interface WorkflowQueueRequestContext { deadlineAtMilliseconds: number; diff --git a/build/cli/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts b/build/cli/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts index 5e08d693..8e9f0f83 100644 --- a/build/cli/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts +++ b/build/cli/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts @@ -2,7 +2,7 @@ export interface GithubWorkflowRunsParameters { owner: string; repo: string; per_page?: number; - workflow_id?: string; + workflow_id: string; status?: string; } export interface GithubWorkflowRunsResponse { @@ -13,8 +13,7 @@ export interface GithubWorkflowRunsResponse { export interface GithubWorkflowRunsClient { rest: { actions: { - listWorkflowRunsForRepo(parameters: GithubWorkflowRunsParameters): Promise; - listWorkflowRuns?(parameters: GithubWorkflowRunsParameters): Promise; + listWorkflowRuns(parameters: GithubWorkflowRunsParameters): Promise; }; }; paginate: { diff --git a/build/github_action/index.js b/build/github_action/index.js index ad64dc6f..c22cb6c0 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -50477,6 +50477,7 @@ const agent_activity_policy_1 = __nccwpck_require__(15375); const main_run_lifecycle_1 = __nccwpck_require__(916); async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, lifecycleStateUseCase, agentActivityUseCase) { (0, logging_ports_1.configureApplicationLogger)((0, logger_adapter_1.createLoggerAdapter)()); + (0, logging_ports_1.setGlobalLoggerDebug)(execution.debug, execution.inputs === undefined); const repository = (0, repository_context_1.requireRepositoryCoordinates)({ owner: execution.owner, repo: execution.repo, @@ -51768,7 +51769,6 @@ const logger_1 = __nccwpck_require__(91151); const main_run_dispatcher_1 = __nccwpck_require__(28586); const workflow_context_1 = __nccwpck_require__(55224); const workflow_queue_composition_root_1 = __nccwpck_require__(21598); -const workflow_queue_policy_1 = __nccwpck_require__(43193); exports.WORKFLOW_QUEUE_FAILURE_MESSAGE = 'Workflow queue check failed; sequential execution was not bypassed.'; /** * Keeps provider diagnostics out of the action's externally visible failure @@ -51782,17 +51782,13 @@ class WorkflowQueueFailureError extends Error { } exports.WorkflowQueueFailureError = WorkflowQueueFailureError; function buildPreviousWorkflowRunsQuery(repository) { + const workflowIdentifier = (0, workflow_context_1.resolveWorkflowIdentifier)(process.env.GITHUB_WORKFLOW_REF); const query = { owner: repository.owner, repository: repository.repo, currentRunId: Number.parseInt(process.env.GITHUB_RUN_ID ?? '', 10), - workflowName: process.env.GITHUB_WORKFLOW ?? '', - workflowNames: workflow_queue_policy_1.COPILOT_WORKFLOW_NAMES, + ...(workflowIdentifier ? { workflowIdentifier } : {}), }; - const workflowIdentifier = (0, workflow_context_1.resolveWorkflowIdentifier)(process.env.GITHUB_WORKFLOW_REF); - if (workflowIdentifier) { - query.workflowIdentifier = workflowIdentifier; - } return query; } async function waitForPreviousWorkflowRuns(token, repository) { @@ -51800,6 +51796,9 @@ async function waitForPreviousWorkflowRuns(token, repository) { if (process.env.GITHUB_ACTIONS === 'true' && !Number.isSafeInteger(query.currentRunId)) { throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.'); } + if (process.env.GITHUB_ACTIONS === 'true' && !query.workflowIdentifier) { + throw new Error('GitHub workflow identifier is unavailable; refusing to bypass sequential execution.'); + } await (0, workflow_queue_composition_root_1.createWaitForPreviousWorkflowRunsUseCase)(token) .invoke(query) .catch(() => { @@ -54666,9 +54665,10 @@ exports.WORKFLOW_QUEUE_POLICY = exports.COPILOT_WORKFLOW_NAMES = void 0; exports.calculateWorkflowPollingDelay = calculateWorkflowPollingDelay; exports.calculateJitteredWorkflowDelay = calculateJitteredWorkflowDelay; /** - * Workflows that execute the Copilot action and therefore share its - * repository mutation queue. Keep these names aligned with workflow `name` - * values in `.github/workflows` and the setup templates. + * Workflows that execute the Copilot action. Keep these names aligned with + * workflow `name` values in `.github/workflows` and the setup templates. + * Queue admission is scoped to the current workflow file; this list is kept + * for workflow-contract validation. */ exports.COPILOT_WORKFLOW_NAMES = [ 'Copilot - Issue', @@ -70417,36 +70417,28 @@ class ActivePreviousWorkflowRunsRepository { if (!Number.isSafeInteger(query.currentRunId)) { throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.'); } - const workflowNames = query.workflowNames?.filter(name => name.trim().length > 0) ?? []; - if (workflowNames.length === 0 && query.workflowName.trim().length === 0) { - throw new Error('GitHub workflow name is unavailable; refusing to bypass sequential execution.'); + const workflowIdentifier = query.workflowIdentifier?.trim() ?? ''; + if (workflowIdentifier.length === 0) { + throw new Error('GitHub workflow identifier is unavailable; refusing to bypass sequential execution.'); } const actions = this.client.rest.actions; - const workflowIdentifier = workflowNames.length === 0 ? query.workflowIdentifier : undefined; - const workflowMethod = workflowIdentifier ? actions.listWorkflowRuns : undefined; - const method = workflowMethod ?? actions.listWorkflowRunsForRepo; + const method = actions.listWorkflowRuns; if (!method) throw new Error('GitHub workflow-scoped runs endpoint is unavailable.'); const parameters = { owner: query.owner, repo: query.repository, per_page: 100, - ...(workflowMethod && workflowIdentifier - ? { workflow_id: workflowIdentifier } - : {}), + workflow_id: workflowIdentifier, }; - const names = workflowNames.length > 0 ? workflowNames : [query.workflowName]; return (0, workflow_runs_retry_1.withWorkflowRunsRetry)(async () => { let activeRunCount = 0; - // Keep one complete sequential traversal: GitHub cannot safely express - // the eight shared workflow names, five active statuses, or the strict - // lower-ID predicate in this endpoint. Do not add provider filters or - // early-stop on page order; a matching run may occur on a later page. - // The residual cost is deep-history pagination, with retries restarting - // from page one, in exchange for an exact fail-closed count. + // The workflow-scoped endpoint limits the traversal to this workflow. + // Keep pagination exhaustive because an active older run may occur on + // a later page, while filtering status and run identity locally. for await (const response of this.client.paginate.iterator(method, parameters)) { activeRunCount += extractWorkflowRuns(response) - .filter(run => isActivePreviousRun(run, query, names)).length; + .filter(run => isActivePreviousRun(run, query)).length; } return activeRunCount; }, { @@ -70469,10 +70461,8 @@ function extractWorkflowRuns(response) { } throw new Error('GitHub workflow runs response did not contain a workflow_runs array.'); } -function isActivePreviousRun(run, query, workflowNames) { - return typeof run.name === 'string' - && workflowNames.includes(run.name) - && run.id < query.currentRunId +function isActivePreviousRun(run, query) { + return run.id < query.currentRunId && workflow_status_1.WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); } diff --git a/build/github_action/src/application/policies/workflow_queue_policy.d.ts b/build/github_action/src/application/policies/workflow_queue_policy.d.ts index 98937d72..eb2732d8 100644 --- a/build/github_action/src/application/policies/workflow_queue_policy.d.ts +++ b/build/github_action/src/application/policies/workflow_queue_policy.d.ts @@ -1,7 +1,8 @@ /** - * Workflows that execute the Copilot action and therefore share its - * repository mutation queue. Keep these names aligned with workflow `name` - * values in `.github/workflows` and the setup templates. + * Workflows that execute the Copilot action. Keep these names aligned with + * workflow `name` values in `.github/workflows` and the setup templates. + * Queue admission is scoped to the current workflow file; this list is kept + * for workflow-contract validation. */ export declare const COPILOT_WORKFLOW_NAMES: readonly ["Copilot - Issue", "Copilot - Issue Comment", "Copilot - Commit", "Copilot - Pull Request", "Copilot - Pull Request Comment", "Copilot - Close Inactive Issues", "Task - Hotfix", "Task - Release"]; export interface WorkflowPollingPolicy { diff --git a/build/github_action/src/application/ports/workflow_run_ports.d.ts b/build/github_action/src/application/ports/workflow_run_ports.d.ts index 5f8c26d4..7f60a54b 100644 --- a/build/github_action/src/application/ports/workflow_run_ports.d.ts +++ b/build/github_action/src/application/ports/workflow_run_ports.d.ts @@ -2,11 +2,8 @@ export interface PreviousWorkflowRunsQuery { owner: string; repository: string; currentRunId: number; - workflowName: string; /** Workflow file name accepted by GitHub to scope the queue query. */ workflowIdentifier?: string; - /** All Copilot workflow names share one queue so different event workflows cannot overlap. */ - workflowNames?: readonly string[]; } export interface WorkflowQueueRequestContext { deadlineAtMilliseconds: number; diff --git a/build/github_action/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts b/build/github_action/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts index 5e08d693..8e9f0f83 100644 --- a/build/github_action/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts +++ b/build/github_action/src/infrastructure/github/ports/github_workflow_provider_ports.d.ts @@ -2,7 +2,7 @@ export interface GithubWorkflowRunsParameters { owner: string; repo: string; per_page?: number; - workflow_id?: string; + workflow_id: string; status?: string; } export interface GithubWorkflowRunsResponse { @@ -13,8 +13,7 @@ export interface GithubWorkflowRunsResponse { export interface GithubWorkflowRunsClient { rest: { actions: { - listWorkflowRunsForRepo(parameters: GithubWorkflowRunsParameters): Promise; - listWorkflowRuns?(parameters: GithubWorkflowRunsParameters): Promise; + listWorkflowRuns(parameters: GithubWorkflowRunsParameters): Promise; }; }; paginate: { diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index 6c541c27..a9106e60 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -64,29 +64,28 @@ downgrade configuration written by a newer one. ## Workflow queue boundary -The repository-wide mutation queue is an application use case backed by semantic -ports. `workflow_queue_policy.ts` owns the eight shared workflow names, the 90-minute -queue budget, adaptive polling schedule, jitter bounds, and retry budgets. The use -case receives a clock and random-value port so deadline and delay behavior remain +The workflow-local queue is an application use case backed by semantic ports. Each +workflow has its own queue; `workflow_queue_policy.ts` owns the 90-minute queue +budget, adaptive polling schedule, jitter bounds, and retry budgets. The use case +receives a clock and random-value port so deadline and delay behavior remain deterministic in tests; concrete system clock/random and timer adapters are wired in `workflow_queue_composition_root.ts`. -`ActivePreviousWorkflowRunsRepository` performs one paginated repository traversal -per poll and filters workflow names, active statuses, and lower run IDs locally. +`ActivePreviousWorkflowRunsRepository` uses GitHub's workflow-scoped runs endpoint +for the current workflow file, paginates only that workflow's history, and filters +active statuses and lower run IDs locally. Provider errors remain at the repository boundary: rate-limited 429/403 responses, transient HTTP/network failures, server wait headers, and malformed pages are classified there and never become a synthetic zero count. Retry logging exposes only sanitized reason, attempt, delay, and reset metadata. Queue-bearing jobs use a 120-minute workflow timeout, leaving 30 minutes of headroom beyond the queue budget. -When a query has a workflow identifier but no workflow-name list, the repository uses -the optional workflow-scoped provider endpoint when available. Repository-only clients -fall back to `listWorkflowRunsForRepo` without sending `workflow_id`; an error from an -invoked provider endpoint is not converted into a capability fallback. Exact counting -still requires exhaustive pagination: the current provider contract cannot express the -eight workflow names, five active statuses, and strict lower-ID predicate as one safe -server-side request. `per_page: 100` and one traversal minimize fan-out, but deep-history -API pressure remains a bounded-retry residual risk rather than a correctness shortcut. +The workflow identifier comes from `GITHUB_WORKFLOW_REF`. A missing identifier or +unavailable workflow-scoped endpoint fails closed instead of falling back to a +repository-wide traversal. Exact counting still requires exhaustive pagination of +the current workflow because the lower run-ID and active-status predicates are +filtered locally. `per_page: 100` keeps the request count bounded by that workflow's +history rather than the entire repository. GitHub Action runs also perform an admission preflight before constructing the full execution. The preflight uses the semantic authenticated-user port and a pure diff --git a/docs/features.mdx b/docs/features.mdx index 9ebdf2da..6fc5a20f 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -136,10 +136,10 @@ Codex is the default runtime for the repository's AI feature paths. OpenCode rem ## Workflow concurrency and sequential execution - **Sequential runs:** An admissible mutation run **waits** for any earlier active Copilot/Task mutation run to finish. Runs execute one after another instead of in parallel or being cancelled — something GitHub does not offer natively. Runs triggered by the PAT owner that would only re-trigger the normal pipeline complete before entering this queue. + **Sequential runs:** An admissible mutation run **waits** for any earlier active run of the same Copilot workflow to finish. Runs execute one after another instead of in parallel or being cancelled — something GitHub does not offer natively. Runs triggered by the PAT owner that would only re-trigger the normal pipeline complete before entering this queue. -GitHub's native [concurrency](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) can cancel in-progress runs when a new one starts (`cancel-in-progress: true`) and can retain only one pending run. Copilot adds an application-level queue: every started Copilot/Task mutation run waits for earlier active runs, including runs from the other Copilot event workflows, so intermediate issue changes are not discarded by a native concurrency group. +GitHub's native [concurrency](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) can cancel in-progress runs when a new one starts (`cancel-in-progress: true`) and can retain only one pending run. Copilot adds an application-level queue per workflow: every started run waits for earlier active runs of that same workflow, so intermediate issue changes are not discarded by a native concurrency group. Release and hotfix workflows use a separate first `queue-gate` job. That job invokes the Action with the internal `queue-gate-only` mode and only the ephemeral @@ -158,19 +158,19 @@ an execution or publish results. ### How it works 1. For a GitHub Action run with a PAT, the action first compares the event actor with the authenticated PAT user. A normal run from the same account completes successfully before project composition, agent provisioning, setup, or queue polling. A valid explicit single action continues through the normal lifecycle. -2. An admitted run resolves the current workflow file from `GITHUB_WORKFLOW_REF` and performs one paginated repository workflow-runs traversal per poll with `per_page: 100`, then locally filters the active statuses (`in_progress`, `queued`, `requested`, `waiting`, and `pending`), the eight known Copilot/Task mutation workflow names, and runs with a **lower run ID** (i.e. started earlier). For compatibility queries that provide a workflow identifier without names, it uses the workflow-scoped endpoint when the provider exposes it; otherwise it uses the repository endpoint without `workflow_id`. +2. An admitted run resolves the current workflow file from `GITHUB_WORKFLOW_REF` and performs one paginated workflow-scoped traversal per poll with `per_page: 100`, then locally filters active statuses (`in_progress`, `queued`, `requested`, `waiting`, and `pending`) and runs with a **lower run ID** (i.e. started earlier). Unrelated workflows are never included. 3. Provider failures fail closed. Transient 408/5xx/network errors use bounded exponential retry; HTTP 429 and rate-limited 403 responses honor `Retry-After` or `x-ratelimit-reset`, then use a slower bounded fallback. Diagnostics contain only the retry reason, attempt, delay, and safe reset timestamp metadata. 4. If any such run exists, the action polls immediately and then uses adaptive 5s, 10s, 20s, 40s, and 60s maximum delays with bounded ±20% jitter. The absolute queue wait is limited to 90 minutes. 5. When no earlier active run in the mutation queue remains, the action continues. A provider failure or queue deadline never becomes an empty result, so setup and mutation work cannot proceed with an unknown queue state. -The queue deliberately traverses every provider page because exact counting must detect -matching runs on later pages. GitHub's current adapter contract cannot safely combine -all eight workflow names, all five active statuses, and the strict lower-ID predicate -in one server-side filter. A full traversal with `per_page: 100` and one sequential -request path reduces fan-out while preserving correctness; deep-history pagination -therefore remains an explicit API-pressure risk, not an early-stop optimization. +The queue deliberately traverses every page of the current workflow because exact +counting must detect a matching run on a later page. The workflow-scoped endpoint +keeps this bounded to one workflow's history, and `per_page: 100` minimizes the +remaining API calls while preserving correctness. -So you get a **repository-wide mutation queue**: multiple triggers for the same workflow (e.g. many issue edits) and related Copilot workflows run sequentially. This conservative scope prevents two workflows from changing shared branches, issue metadata, or release state at the same time. Read-only CI may keep its own native concurrency policy. +So you get a **workflow-local mutation queue**: multiple triggers for the same +workflow (e.g. many issue edits) run sequentially without blocking unrelated +Copilot workflows. Read-only CI may keep its own native concurrency policy. ### Example diff --git a/src/actions/__tests__/common_action.test.ts b/src/actions/__tests__/common_action.test.ts index 5b81a75b..beeea746 100644 --- a/src/actions/__tests__/common_action.test.ts +++ b/src/actions/__tests__/common_action.test.ts @@ -29,6 +29,7 @@ jest.mock('../../utils/logger', () => ({ logInfo: jest.fn(), logError: jest.fn(), logDebugInfo: jest.fn(), + setGlobalLoggerDebug: jest.fn(), clearAccumulatedLogs: jest.fn(), })); @@ -211,18 +212,7 @@ describe('mainRun', () => { owner: 'org', repository: 'repo', currentRunId: 200, - workflowName: 'CI', workflowIdentifier: 'copilot_issue.yml', - workflowNames: [ - 'Copilot - Issue', - 'Copilot - Issue Comment', - 'Copilot - Commit', - 'Copilot - Pull Request', - 'Copilot - Pull Request Comment', - 'Copilot - Close Inactive Issues', - 'Task - Hotfix', - 'Task - Release', - ], }); }); @@ -251,6 +241,18 @@ describe('mainRun', () => { expect(mockSetupExecutionInvoke).not.toHaveBeenCalled(); }); + it('fails closed when a GitHub Actions run has no workflow identifier', async () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_RUN_ID = '200'; + delete process.env.GITHUB_WORKFLOW_REF; + + await expect(runMain(mockExecution({ welcome: undefined }))).rejects.toThrow( + 'GitHub workflow identifier is unavailable; refusing to bypass sequential execution.', + ); + expect(createWaitForPreviousWorkflowRunsUseCase).not.toHaveBeenCalled(); + expect(mockSetupExecutionInvoke).not.toHaveBeenCalled(); + }); + it('skips wait when welcome is set', async () => { const execution = mockExecution({ welcome: { title: 'Hi', messages: ['Welcome'] }, diff --git a/src/actions/common_action.ts b/src/actions/common_action.ts index 8504559f..0ab470d0 100644 --- a/src/actions/common_action.ts +++ b/src/actions/common_action.ts @@ -7,7 +7,7 @@ import { resolveMainRunRoute } from './main_run_route'; import { createSetupExecutionUseCase } from '../infrastructure/composition/execution_setup_composition_root'; import { createMainRunRouteCompositionRoot } from '../infrastructure/composition/main_run_route_composition_root'; import { requireRepositoryCoordinates } from './repository_context'; -import { configureApplicationLogger } from '../application/ports/logging_ports'; +import { configureApplicationLogger, setGlobalLoggerDebug } from '../application/ports/logging_ports'; import { createLoggerAdapter } from '../infrastructure/logging/logger_adapter'; import type { SynchronizeLifecycleStateUseCase } from '../application/usecases/actions/synchronize_lifecycle_state_use_case'; import type { SynchronizeAgentActivityUseCase } from '../application/usecases/actions/synchronize_agent_activity_use_case'; @@ -28,6 +28,7 @@ export async function mainRun( agentActivityUseCase?: SynchronizeAgentActivityUseCase, ): Promise { configureApplicationLogger(createLoggerAdapter()); + setGlobalLoggerDebug(execution.debug, execution.inputs === undefined); const repository = requireRepositoryCoordinates({ owner: execution.owner, repo: execution.repo, diff --git a/src/actions/main_run_lifecycle.ts b/src/actions/main_run_lifecycle.ts index 585a6566..7d2bb750 100644 --- a/src/actions/main_run_lifecycle.ts +++ b/src/actions/main_run_lifecycle.ts @@ -10,7 +10,6 @@ import type { ExecutableMainRunRoute, MainRunRouteHandlers } from './main_run_ro import type { RepositoryCoordinates } from './repository_context'; import { resolveWorkflowIdentifier } from './workflow_context'; import { createWaitForPreviousWorkflowRunsUseCase } from '../infrastructure/composition/workflow_queue_composition_root'; -import { COPILOT_WORKFLOW_NAMES } from '../application/policies/workflow_queue_policy'; import type { PreviousWorkflowRunsQuery } from '../application/ports/workflow_run_ports'; export const WORKFLOW_QUEUE_FAILURE_MESSAGE = @@ -30,18 +29,13 @@ export class WorkflowQueueFailureError extends Error { export function buildPreviousWorkflowRunsQuery( repository: RepositoryCoordinates, ): PreviousWorkflowRunsQuery { + const workflowIdentifier = resolveWorkflowIdentifier(process.env.GITHUB_WORKFLOW_REF); const query: PreviousWorkflowRunsQuery = { owner: repository.owner, repository: repository.repo, currentRunId: Number.parseInt(process.env.GITHUB_RUN_ID ?? '', 10), - workflowName: process.env.GITHUB_WORKFLOW ?? '', - workflowNames: COPILOT_WORKFLOW_NAMES, + ...(workflowIdentifier ? { workflowIdentifier } : {}), }; - const workflowIdentifier = resolveWorkflowIdentifier(process.env.GITHUB_WORKFLOW_REF); - if (workflowIdentifier) { - query.workflowIdentifier = workflowIdentifier; - } - return query; } @@ -53,6 +47,9 @@ export async function waitForPreviousWorkflowRuns( if (process.env.GITHUB_ACTIONS === 'true' && !Number.isSafeInteger(query.currentRunId)) { throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.'); } + if (process.env.GITHUB_ACTIONS === 'true' && !query.workflowIdentifier) { + throw new Error('GitHub workflow identifier is unavailable; refusing to bypass sequential execution.'); + } await createWaitForPreviousWorkflowRunsUseCase(token) .invoke(query) .catch(() => { diff --git a/src/application/policies/workflow_queue_policy.ts b/src/application/policies/workflow_queue_policy.ts index bd8d9a9f..7f132d83 100644 --- a/src/application/policies/workflow_queue_policy.ts +++ b/src/application/policies/workflow_queue_policy.ts @@ -1,7 +1,8 @@ /** - * Workflows that execute the Copilot action and therefore share its - * repository mutation queue. Keep these names aligned with workflow `name` - * values in `.github/workflows` and the setup templates. + * Workflows that execute the Copilot action. Keep these names aligned with + * workflow `name` values in `.github/workflows` and the setup templates. + * Queue admission is scoped to the current workflow file; this list is kept + * for workflow-contract validation. */ export const COPILOT_WORKFLOW_NAMES = [ 'Copilot - Issue', diff --git a/src/application/ports/workflow_run_ports.ts b/src/application/ports/workflow_run_ports.ts index 41b78f22..2f56f1cd 100644 --- a/src/application/ports/workflow_run_ports.ts +++ b/src/application/ports/workflow_run_ports.ts @@ -2,11 +2,8 @@ export interface PreviousWorkflowRunsQuery { owner: string; repository: string; currentRunId: number; - workflowName: string; /** Workflow file name accepted by GitHub to scope the queue query. */ workflowIdentifier?: string; - /** All Copilot workflow names share one queue so different event workflows cannot overlap. */ - workflowNames?: readonly string[]; } export interface WorkflowQueueRequestContext { diff --git a/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts b/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts index e74d6da3..1170158a 100644 --- a/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts +++ b/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts @@ -13,7 +13,7 @@ const query: PreviousWorkflowRunsQuery = { owner: 'org', repository: 'repo', currentRunId: 200, - workflowName: 'Copilot - Issue', + workflowIdentifier: 'copilot_issue.yml', }; function observer(): jest.Mocked { @@ -145,4 +145,4 @@ describe('WaitForPreviousWorkflowRunsUseCase', () => { ).invoke(query)).rejects.toThrow('Timeout waiting for previous runs to finish.'); expect(queryPort.countActivePreviousRuns).not.toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts b/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts index 3959f877..0d635453 100644 --- a/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts +++ b/src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts @@ -5,14 +5,12 @@ import type { GithubWorkflowRunsResponse, } from '../../../../infrastructure/github/ports/github_workflow_provider_ports'; import { ActivePreviousWorkflowRunsRepository } from '../active_previous_workflow_runs_repository'; -import { COPILOT_WORKFLOW_NAMES } from '../../../../application/policies/workflow_queue_policy'; import { WORKFLOW_ACTIVE_STATUSES } from '../workflow_status'; -const listWorkflowRunsForRepo = jest.fn(); const listWorkflowRuns = jest.fn(); const iterator = jest.fn(); const client = { - rest: { actions: { listWorkflowRunsForRepo, listWorkflowRuns } }, + rest: { actions: { listWorkflowRuns } }, paginate: { iterator }, } as unknown as GithubWorkflowRunsClient; @@ -24,8 +22,7 @@ const query = { owner: 'org', repository: 'repo', currentRunId: 200, - workflowName: 'Copilot - Issue', - workflowNames: ['Copilot - Issue', 'Task - Release'], + workflowIdentifier: 'copilot_issue.yml', }; describe('ActivePreviousWorkflowRunsRepository', () => { @@ -40,16 +37,25 @@ describe('ActivePreviousWorkflowRunsRepository', () => { expect(iterator).not.toHaveBeenCalled(); }); - it('uses one repository traversal and locally filters mixed statuses and workflow names', async () => { + it('fails closed without a workflow identifier', async () => { + const repository = new ActivePreviousWorkflowRunsRepository(client); + + await expect(repository.countActivePreviousRuns({ ...query, workflowIdentifier: undefined })).rejects.toThrow( + 'GitHub workflow identifier is unavailable; refusing to bypass sequential execution.', + ); + expect(iterator).not.toHaveBeenCalled(); + }); + + it('uses the workflow-scoped endpoint and locally filters active previous runs', async () => { iterator.mockImplementation(async function* () { yield { data: { workflow_runs: [ workflowRun({ id: 199, name: 'Copilot - Issue', status: WORKFLOW_STATUS.IN_PROGRESS }), - workflowRun({ id: 198, name: 'Task - Release', status: WORKFLOW_STATUS.QUEUED }), + workflowRun({ id: 198, name: 'Copilot - Issue', status: WORKFLOW_STATUS.QUEUED }), workflowRun({ id: 200, name: 'Copilot - Issue', status: WORKFLOW_STATUS.IN_PROGRESS }), workflowRun({ id: 201, name: 'Copilot - Issue', status: WORKFLOW_STATUS.IN_PROGRESS }), - workflowRun({ id: 197, name: 'Unrelated workflow', status: WORKFLOW_STATUS.IN_PROGRESS }), + workflowRun({ id: 197, name: 'Copilot - Issue', status: WORKFLOW_STATUS.IN_PROGRESS }), workflowRun({ id: 196, name: 'Copilot - Issue', status: WORKFLOW_STATUS.COMPLETED }), ], }, @@ -57,22 +63,23 @@ describe('ActivePreviousWorkflowRunsRepository', () => { }); const repository = new ActivePreviousWorkflowRunsRepository(client); - await expect(repository.countActivePreviousRuns(query)).resolves.toBe(2); + await expect(repository.countActivePreviousRuns(query)).resolves.toBe(3); expect(iterator).toHaveBeenCalledTimes(1); - expect(iterator).toHaveBeenCalledWith(listWorkflowRunsForRepo, { + expect(iterator).toHaveBeenCalledWith(listWorkflowRuns, { owner: 'org', repo: 'repo', per_page: 100, + workflow_id: 'copilot_issue.yml', }); }); - it('detects an older active matching run on a later page', async () => { + it('detects an older active run on a later page', async () => { iterator.mockImplementation(async function* () { yield { data: { workflow_runs: [] } } as GithubWorkflowRunsResponse; yield { data: { workflow_runs: [ - workflowRun({ id: 150, name: 'Task - Release', status: WORKFLOW_STATUS.WAITING }), + workflowRun({ id: 150, name: 'Copilot - Issue', status: WORKFLOW_STATUS.WAITING }), ], }, } as GithubWorkflowRunsResponse; @@ -83,54 +90,42 @@ describe('ActivePreviousWorkflowRunsRepository', () => { expect(iterator).toHaveBeenCalledTimes(1); }); - it('counts all eight shared workflow names and five active statuses across every page', async () => { - const runs = COPILOT_WORKFLOW_NAMES.flatMap((name, nameIndex) => WORKFLOW_ACTIVE_STATUSES.map((status, statusIndex) => workflowRun({ - id: 1 + nameIndex * WORKFLOW_ACTIVE_STATUSES.length + statusIndex, - name, + it('counts all active statuses across every page of the current workflow', async () => { + const runs = WORKFLOW_ACTIVE_STATUSES.map((status, index) => workflowRun({ + id: 1 + index, + name: 'Copilot - Issue', status, - }))); + })); iterator.mockImplementation(async function* () { - yield { data: { workflow_runs: runs.slice(0, 17) } } as GithubWorkflowRunsResponse; - yield { data: { workflow_runs: runs.slice(17) } } as GithubWorkflowRunsResponse; + yield { data: { workflow_runs: runs.slice(0, 2) } } as GithubWorkflowRunsResponse; + yield { data: { workflow_runs: runs.slice(2) } } as GithubWorkflowRunsResponse; }); const repository = new ActivePreviousWorkflowRunsRepository(client); - await expect(repository.countActivePreviousRuns({ - ...query, - workflowNames: [...COPILOT_WORKFLOW_NAMES], - })).resolves.toBe(COPILOT_WORKFLOW_NAMES.length * WORKFLOW_ACTIVE_STATUSES.length); + await expect(repository.countActivePreviousRuns(query)).resolves.toBe(WORKFLOW_ACTIVE_STATUSES.length); expect(iterator).toHaveBeenCalledTimes(1); }); - it('counts multiple earlier queue runs while excluding cancelled and skipped terminals', async () => { + it('excludes cancelled and skipped terminal runs', async () => { iterator.mockImplementation(async function* () { yield { data: { workflow_runs: [ workflowRun({ id: 199, name: 'Copilot - Issue', status: WORKFLOW_STATUS.IN_PROGRESS }), - workflowRun({ id: 198, name: 'Task - Release', status: WORKFLOW_STATUS.QUEUED }), - workflowRun({ id: 197, name: 'Copilot - Commit', status: WORKFLOW_STATUS.CANCELLED }), - workflowRun({ id: 196, name: 'Copilot - Pull Request', status: WORKFLOW_STATUS.SKIPPED }), - workflowRun({ id: 200, name: 'Copilot - Issue Comment', status: WORKFLOW_STATUS.IN_PROGRESS }), + workflowRun({ id: 198, name: 'Copilot - Issue', status: WORKFLOW_STATUS.QUEUED }), + workflowRun({ id: 197, name: 'Copilot - Issue', status: WORKFLOW_STATUS.CANCELLED }), + workflowRun({ id: 196, name: 'Copilot - Issue', status: WORKFLOW_STATUS.SKIPPED }), + workflowRun({ id: 200, name: 'Copilot - Issue', status: WORKFLOW_STATUS.IN_PROGRESS }), ], }, } as GithubWorkflowRunsResponse; }); const repository = new ActivePreviousWorkflowRunsRepository(client); - await expect(repository.countActivePreviousRuns({ - ...query, - workflowNames: [ - 'Copilot - Issue', - 'Copilot - Issue Comment', - 'Task - Release', - 'Copilot - Commit', - 'Copilot - Pull Request', - ], - })).resolves.toBe(2); + await expect(repository.countActivePreviousRuns(query)).resolves.toBe(2); }); - it('supports both Octokit page shapes and the compatibility workflow endpoint', async () => { + it('supports both Octokit page shapes from the workflow endpoint', async () => { iterator.mockImplementation(async function* () { yield { data: [workflowRun({ id: 199, name: 'Copilot - Issue', status: WORKFLOW_STATUS.PENDING })], @@ -138,11 +133,7 @@ describe('ActivePreviousWorkflowRunsRepository', () => { }); const repository = new ActivePreviousWorkflowRunsRepository(client); - await expect(repository.countActivePreviousRuns({ - ...query, - workflowNames: undefined, - workflowIdentifier: 'copilot_issue.yml', - })).resolves.toBe(1); + await expect(repository.countActivePreviousRuns(query)).resolves.toBe(1); expect(iterator).toHaveBeenCalledWith(listWorkflowRuns, { owner: 'org', repo: 'repo', @@ -151,30 +142,17 @@ describe('ActivePreviousWorkflowRunsRepository', () => { }); }); - it('falls back to repository traversal when the workflow endpoint is unavailable', async () => { - iterator.mockImplementation(async function* () { - yield { - data: { - workflow_runs: [workflowRun({ id: 199, name: 'Copilot - Issue', status: WORKFLOW_STATUS.PENDING })], - }, - } as GithubWorkflowRunsResponse; - }); - const fallbackClient = { - rest: { actions: { listWorkflowRunsForRepo } }, + it('fails closed when the workflow-scoped endpoint is unavailable', async () => { + const unsupportedClient = { + rest: { actions: {} }, paginate: { iterator }, } as unknown as GithubWorkflowRunsClient; - const repository = new ActivePreviousWorkflowRunsRepository(fallbackClient); - - await expect(repository.countActivePreviousRuns({ - ...query, - workflowNames: undefined, - workflowIdentifier: 'copilot_issue.yml', - })).resolves.toBe(1); - expect(iterator).toHaveBeenCalledWith(listWorkflowRunsForRepo, { - owner: 'org', - repo: 'repo', - per_page: 100, - }); + const repository = new ActivePreviousWorkflowRunsRepository(unsupportedClient); + + await expect(repository.countActivePreviousRuns(query)).rejects.toThrow( + 'GitHub workflow-scoped runs endpoint is unavailable', + ); + expect(iterator).not.toHaveBeenCalled(); }); it('does not switch endpoints after a scoped provider failure', async () => { @@ -184,18 +162,13 @@ describe('ActivePreviousWorkflowRunsRepository', () => { }); const repository = new ActivePreviousWorkflowRunsRepository(client); - await expect(repository.countActivePreviousRuns({ - ...query, - workflowNames: undefined, - workflowIdentifier: 'copilot_issue.yml', - })).rejects.toMatchObject({ status: 404 }); + await expect(repository.countActivePreviousRuns(query)).rejects.toMatchObject({ status: 404 }); expect(iterator).toHaveBeenCalledWith(listWorkflowRuns, { owner: 'org', repo: 'repo', per_page: 100, workflow_id: 'copilot_issue.yml', }); - expect(iterator).not.toHaveBeenCalledWith(listWorkflowRunsForRepo, expect.anything()); }); it('rejects malformed provider pages instead of treating them as empty', async () => { @@ -218,7 +191,7 @@ describe('ActivePreviousWorkflowRunsRepository', () => { yield { data: { workflow_runs: [] } } as GithubWorkflowRunsResponse; if (traversals === 1) throw { status: 500 }; yield { - data: { workflow_runs: [workflowRun({ id: 150, name: 'Task - Release', status: WORKFLOW_STATUS.QUEUED })] }, + data: { workflow_runs: [workflowRun({ id: 150, name: 'Copilot - Issue', status: WORKFLOW_STATUS.QUEUED })] }, } as GithubWorkflowRunsResponse; }); const repository = new ActivePreviousWorkflowRunsRepository(client, retryDelayPort, { diff --git a/src/data/repository/workflow/active_previous_workflow_runs_repository.ts b/src/data/repository/workflow/active_previous_workflow_runs_repository.ts index 3488bd37..24ee45d1 100644 --- a/src/data/repository/workflow/active_previous_workflow_runs_repository.ts +++ b/src/data/repository/workflow/active_previous_workflow_runs_repository.ts @@ -36,36 +36,28 @@ export class ActivePreviousWorkflowRunsRepository implements PreviousWorkflowRun if (!Number.isSafeInteger(query.currentRunId)) { throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.'); } - const workflowNames = query.workflowNames?.filter(name => name.trim().length > 0) ?? []; - if (workflowNames.length === 0 && query.workflowName.trim().length === 0) { - throw new Error('GitHub workflow name is unavailable; refusing to bypass sequential execution.'); + const workflowIdentifier = query.workflowIdentifier?.trim() ?? ''; + if (workflowIdentifier.length === 0) { + throw new Error('GitHub workflow identifier is unavailable; refusing to bypass sequential execution.'); } const actions = this.client.rest.actions; - const workflowIdentifier = workflowNames.length === 0 ? query.workflowIdentifier : undefined; - const workflowMethod = workflowIdentifier ? actions.listWorkflowRuns : undefined; - const method = workflowMethod ?? actions.listWorkflowRunsForRepo; + const method = actions.listWorkflowRuns; if (!method) throw new Error('GitHub workflow-scoped runs endpoint is unavailable.'); const parameters = { owner: query.owner, repo: query.repository, per_page: 100, - ...(workflowMethod && workflowIdentifier - ? { workflow_id: workflowIdentifier } - : {}), + workflow_id: workflowIdentifier, }; - const names = workflowNames.length > 0 ? workflowNames : [query.workflowName]; return withWorkflowRunsRetry(async () => { let activeRunCount = 0; - // Keep one complete sequential traversal: GitHub cannot safely express - // the eight shared workflow names, five active statuses, or the strict - // lower-ID predicate in this endpoint. Do not add provider filters or - // early-stop on page order; a matching run may occur on a later page. - // The residual cost is deep-history pagination, with retries restarting - // from page one, in exchange for an exact fail-closed count. + // The workflow-scoped endpoint limits the traversal to this workflow. + // Keep pagination exhaustive because an active older run may occur on + // a later page, while filtering status and run identity locally. for await (const response of this.client.paginate.iterator(method, parameters)) { activeRunCount += extractWorkflowRuns(response) - .filter(run => isActivePreviousRun(run, query, names)).length; + .filter(run => isActivePreviousRun(run, query)).length; } return activeRunCount; }, { @@ -91,10 +83,7 @@ function extractWorkflowRuns(response: GithubWorkflowRunsResponse): GithubWorkfl function isActivePreviousRun( run: GithubWorkflowRun, query: PreviousWorkflowRunsQuery, - workflowNames: readonly string[], ): boolean { - return typeof run.name === 'string' - && workflowNames.includes(run.name) - && run.id < query.currentRunId + return run.id < query.currentRunId && WORKFLOW_ACTIVE_STATUSES.includes(run.status ?? 'unknown'); } diff --git a/src/infrastructure/composition/__tests__/workflow_queue_composition_root.test.ts b/src/infrastructure/composition/__tests__/workflow_queue_composition_root.test.ts index 6294d7f7..a1a8895d 100644 --- a/src/infrastructure/composition/__tests__/workflow_queue_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/workflow_queue_composition_root.test.ts @@ -8,12 +8,12 @@ describe('workflow queue composition root', () => { beforeEach(() => jest.clearAllMocks()); it('wires the active-run query to the GitHub workflow client', async () => { - const listWorkflowRunsForRepo = jest.fn(); + const listWorkflowRuns = jest.fn(); const iterator = jest.fn().mockImplementation(async function* () { yield { data: { workflow_runs: [] } }; }); (github.getOctokit as jest.Mock).mockReturnValue({ - rest: { actions: { listWorkflowRunsForRepo } }, + rest: { actions: { listWorkflowRuns } }, paginate: { iterator }, }); @@ -22,27 +22,27 @@ describe('workflow queue composition root', () => { owner: 'org', repository: 'repo', currentRunId: 200, - workflowName: 'CI', + workflowIdentifier: 'copilot_issue.yml', }); expect(github.getOctokit).toHaveBeenCalledWith('token'); expect(github.getOctokit).toHaveBeenCalledTimes(1); - expect(iterator).toHaveBeenCalledWith(listWorkflowRunsForRepo, { + expect(iterator).toHaveBeenCalledWith(listWorkflowRuns, { owner: 'org', repo: 'repo', per_page: 100, + workflow_id: 'copilot_issue.yml', }); expect(iterator).toHaveBeenCalledTimes(1); }); it('uses the workflow-scoped endpoint when the workflow identifier is available', async () => { - const listWorkflowRunsForRepo = jest.fn(); const listWorkflowRuns = jest.fn(); const iterator = jest.fn().mockImplementation(async function* () { yield { data: [] }; }); (github.getOctokit as jest.Mock).mockReturnValue({ - rest: { actions: { listWorkflowRunsForRepo, listWorkflowRuns } }, + rest: { actions: { listWorkflowRuns } }, paginate: { iterator }, }); @@ -51,7 +51,6 @@ describe('workflow queue composition root', () => { owner: 'org', repository: 'repo', currentRunId: 200, - workflowName: 'CI', workflowIdentifier: 'copilot_issue.yml', }); @@ -61,6 +60,5 @@ describe('workflow queue composition root', () => { per_page: 100, workflow_id: 'copilot_issue.yml', }); - expect(iterator).not.toHaveBeenCalledWith(listWorkflowRunsForRepo, expect.anything()); }); }); diff --git a/src/infrastructure/github/ports/github_workflow_provider_ports.ts b/src/infrastructure/github/ports/github_workflow_provider_ports.ts index 592fc3ce..58183b54 100644 --- a/src/infrastructure/github/ports/github_workflow_provider_ports.ts +++ b/src/infrastructure/github/ports/github_workflow_provider_ports.ts @@ -2,7 +2,7 @@ export interface GithubWorkflowRunsParameters { owner: string; repo: string; per_page?: number; - workflow_id?: string; + workflow_id: string; status?: string; } @@ -13,8 +13,7 @@ export interface GithubWorkflowRunsResponse { export interface GithubWorkflowRunsClient { rest: { actions: { - listWorkflowRunsForRepo(parameters: GithubWorkflowRunsParameters): Promise; - listWorkflowRuns?(parameters: GithubWorkflowRunsParameters): Promise; + listWorkflowRuns(parameters: GithubWorkflowRunsParameters): Promise; }; }; paginate: {