Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 166 additions & 23 deletions build/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47252,6 +47252,7 @@ const dotenv = __importStar(__nccwpck_require__(2437));
const local_action_1 = __nccwpck_require__(7002);
const issue_repository_1 = __nccwpck_require__(57);
const constants_1 = __nccwpck_require__(8593);
const setup_files_1 = __nccwpck_require__(1666);
const logger_1 = __nccwpck_require__(8836);
const prompts_1 = __nccwpck_require__(5554);
const ai_1 = __nccwpck_require__(4470);
Expand Down Expand Up @@ -47650,12 +47651,27 @@ program
process.exit(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 <your_github_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;
}
(0, logger_1.logInfo)('βš™οΈ Running initial setup (labels, issue types, access)...');
const params = {
[constants_1.INPUT_KEYS.DEBUG]: options.debug.toString(),
[constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.INITIAL_SETUP,
[constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: 1,
[constants_1.INPUT_KEYS.TOKEN]: options.token || process.env.PERSONAL_ACCESS_TOKEN,
[constants_1.INPUT_KEYS.TOKEN]: token,
repo: {
owner: gitInfo.owner,
repo: gitInfo.repo,
Expand Down Expand Up @@ -48712,6 +48728,21 @@ class ProjectDetail {
this.url = data[`url`] ?? '';
this.number = data[`number`] ?? -1;
}
/**
* Returns the full public URL to the project (board).
* Uses the URL from the API when present and valid; otherwise builds it from owner, type and number.
* Returns empty string when project number is invalid (e.g. missing from API).
*/
get publicUrl() {
if (this.url && typeof this.url === 'string' && this.url.startsWith('https://')) {
return this.url;
}
if (typeof this.number !== 'number' || this.number <= 0) {
return '';
}
const path = this.type === 'organization' ? 'orgs' : 'users';
return `https://github.com/${path}/${this.owner}/projects/${this.number}`;
}
}
exports.ProjectDetail = ProjectDetail;

Expand Down Expand Up @@ -51556,14 +51587,22 @@ class ProjectRepository {
number: issueOrPullRequestNumber
});
if (!issueOrPrResult.repository.issueOrPullRequest) {
console.error(`Issue or PR #${issueOrPullRequestNumber} not found.`);
(0, logger_1.logError)(`Issue or PR #${issueOrPullRequestNumber} not found in repository.`);
return undefined;
}
const contentId = issueOrPrResult.repository.issueOrPullRequest.id;
// Search for the item ID in the project with pagination
let cursor = null;
let projectItemId = undefined;
let totalItemsChecked = 0;
const maxPages = 100; // 100 * 100 = 10_000 items max to avoid runaway loops
let pageCount = 0;
do {
if (pageCount >= maxPages) {
(0, logger_1.logError)(`Stopped after ${maxPages} pages (${totalItemsChecked} items). Issue or PR #${issueOrPullRequestNumber} not found in project.`);
break;
}
pageCount += 1;
const projectQuery = `
query($projectId: ID!, $cursor: String) {
node(id: $projectId) {
Expand Down Expand Up @@ -51592,16 +51631,36 @@ class ProjectRepository {
projectId: project.id,
cursor
});
const items = projectResult.node.items.nodes;
if (projectResult.node === null) {
(0, logger_1.logError)(`Project not found for ID "${project.id}". Ensure the project is loaded via getProjectDetail (GraphQL node ID), not the project number.`);
throw new Error(`Project not found or invalid project ID. The project ID must be the GraphQL node ID from the API (e.g. PVT_...), not the project number.`);
}
const items = projectResult.node.items?.nodes ?? [];
totalItemsChecked += items.length;
const pageInfo = projectResult.node.items?.pageInfo;
const foundItem = items.find((item) => item.content?.id === contentId);
if (foundItem) {
projectItemId = foundItem.id;
break;
}
cursor = projectResult.node.items.pageInfo.hasNextPage
? projectResult.node.items.pageInfo.endCursor
: null;
// Advance cursor only when there is a next page AND a non-null cursor (avoid missing pages)
const hasNextPage = pageInfo?.hasNextPage === true;
const endCursor = pageInfo?.endCursor ?? null;
if (hasNextPage && endCursor) {
cursor = endCursor;
}
else {
if (hasNextPage && !endCursor) {
(0, logger_1.logError)(`Project items pagination: hasNextPage is true but endCursor is null (page ${pageCount}, ${totalItemsChecked} items so far). Cannot fetch more.`);
}
cursor = null;
}
} while (cursor);
if (projectItemId === undefined) {
(0, logger_1.logError)(`Issue or PR #${issueOrPullRequestNumber} not found in project after checking ${totalItemsChecked} items (${pageCount} page(s)). ` +
`Link it to the project first, or wait for the board to sync.`);
throw new Error(`Issue or pull request #${issueOrPullRequestNumber} is not in the project yet (checked ${totalItemsChecked} items). Link it to the project first, or wait for the board to sync.`);
}
return projectItemId;
};
this.isContentLinked = async (project, contentId, token) => {
Expand Down Expand Up @@ -54003,6 +54062,18 @@ class InitialSetupUseCase {
(0, setup_files_1.ensureGitHubDirs)(process.cwd());
const filesResult = (0, setup_files_1.copySetupFiles)(process.cwd());
steps.push(`βœ… Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`);
if (!(0, setup_files_1.hasValidSetupToken)(process.cwd())) {
(0, logger_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.');
results.push(new result_1.Result({
id: this.taskId,
success: false,
executed: true,
steps: steps,
errors: errors,
}));
return results;
}
// 1. Verificar acceso a GitHub con Personal Access Token
(0, logger_1.logInfo)('πŸ” Checking GitHub access...');
const githubAccessResult = await this.verifyGitHubAccess(param);
Expand Down Expand Up @@ -57923,7 +57994,7 @@ class CheckPriorityIssueSizeUseCase {
success: true,
executed: true,
steps: [
`Priority set to \`${priorityLabel}\` in [${project.title}](https://github.com/${param.owner}/${param.repo}/projects/${project.id}).`,
`Priority set to \`${priorityLabel}\` in [${project.title}](${project.publicUrl}).`,
],
}));
}
Expand Down Expand Up @@ -58364,7 +58435,7 @@ class MoveIssueToInProgressUseCase {
success: true,
executed: true,
steps: [
`Moved issue to \`${columnName}\` in [${project.title}](https://github.com/${param.owner}/${param.repo}/projects/${project.id}).`,
`Moved issue to \`${columnName}\` in [${project.title}](${project.publicUrl}).`,
],
}));
}
Expand Down Expand Up @@ -59116,7 +59187,7 @@ class CheckPriorityPullRequestSizeUseCase {
success: true,
executed: true,
steps: [
`Priority set to \`${priorityLabel}\` in [${project.title}](https://github.com/${param.owner}/${param.repo}/projects/${project.id}).`,
`Priority set to \`${priorityLabel}\` in [${project.title}](${project.publicUrl}).`,
],
}));
}
Expand Down Expand Up @@ -60342,6 +60413,10 @@ var __importStar = (this && this.__importStar) || (function () {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ensureGitHubDirs = ensureGitHubDirs;
exports.copySetupFiles = copySetupFiles;
exports.ensureEnvWithToken = ensureEnvWithToken;
exports.getSetupToken = getSetupToken;
exports.hasValidSetupToken = hasValidSetupToken;
exports.setupEnvFileExists = setupEnvFileExists;
const fs = __importStar(__nccwpck_require__(7147));
const path = __importStar(__nccwpck_require__(1017));
const logger_1 = __nccwpck_require__(8836);
Expand Down Expand Up @@ -60370,11 +60445,13 @@ function ensureGitHubDirs(cwd) {
* Copy setup files from setup/ to repo (.github/ workflows, ISSUE_TEMPLATE, pull_request_template.md, .env at root).
* Skips files that already exist at destination (no overwrite).
* Logs each file copied or skipped. No-op if setup/ does not exist.
* @param cwd - Repo root
* By default setup dir is the copilot package root (not cwd), so it works when running from another repo.
* @param cwd - Repo root (destination)
* @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root.
* @returns { copied, skipped }
*/
function copySetupFiles(cwd) {
const setupDir = path.join(cwd, 'setup');
function copySetupFiles(cwd, setupDirOverride) {
const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup');
if (!fs.existsSync(setupDir))
return { copied: 0, skipped: 0 };
let copied = 0;
Expand Down Expand Up @@ -60430,20 +60507,80 @@ function copySetupFiles(cwd) {
copied += 1;
}
}
const envSrc = path.join(setupDir, '.env');
const envDst = path.join(cwd, '.env');
if (fs.existsSync(envSrc) && fs.statSync(envSrc).isFile()) {
if (fs.existsSync(envDst)) {
(0, logger_1.logInfo)(' ⏭️ .env already exists; skipping.');
skipped += 1;
ensureEnvWithToken(cwd);
return { copied, skipped };
}
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 {
fs.copyFileSync(envSrc, envDst);
(0, logger_1.logInfo)(' βœ… Copied setup/.env β†’ .env');
copied += 1;
(0, logger_1.logInfo)(' ⚠️ .env exists but PERSONAL_ACCESS_TOKEN is missing or empty.');
}
return;
}
return { copied, skipped };
(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;
}
/**
* 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.
* Returns undefined if no valid token is found.
*/
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;
}
/**
* 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.
*/
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();
}


Expand Down Expand Up @@ -60650,8 +60787,14 @@ exports.getActionInputsWithDefaults = getActionInputsWithDefaults;
const fs = __importStar(__nccwpck_require__(7147));
const path = __importStar(__nccwpck_require__(1017));
const yaml = __importStar(__nccwpck_require__(1917));
/**
* Resolves action.yml from the copilot package root, not cwd.
* When run as CLI from another repo, cwd is that repo; action.yml lives next to the bundle.
* - From source: __dirname is src/utils β†’ ../../action.yml = repo root.
* - From bundle (build/cli): __dirname is bundle dir β†’ ../../action.yml = package root.
*/
function loadActionYaml() {
const actionYamlPath = path.join(process.cwd(), 'action.yml');
const actionYamlPath = path.join(__dirname, '..', '..', 'action.yml');
const yamlContent = fs.readFileSync(actionYamlPath, 'utf8');
return yaml.load(yamlContent);
}
Expand Down
6 changes: 6 additions & 0 deletions build/cli/src/data/model/project_detail.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ export declare class ProjectDetail {
url: string;
number: number;
constructor(data: any);
/**
* Returns the full public URL to the project (board).
* Uses the URL from the API when present and valid; otherwise builds it from owner, type and number.
* Returns empty string when project number is invalid (e.g. missing from API).
*/
get publicUrl(): string;
}
25 changes: 23 additions & 2 deletions build/cli/src/utils/setup_files.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,31 @@ export declare function ensureGitHubDirs(cwd: string): void;
* Copy setup files from setup/ to repo (.github/ workflows, ISSUE_TEMPLATE, pull_request_template.md, .env at root).
* Skips files that already exist at destination (no overwrite).
* Logs each file copied or skipped. No-op if setup/ does not exist.
* @param cwd - Repo root
* By default setup dir is the copilot package root (not cwd), so it works when running from another repo.
* @param cwd - Repo root (destination)
* @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root.
* @returns { copied, skipped }
*/
export declare function copySetupFiles(cwd: string): {
export declare function copySetupFiles(cwd: string, setupDirOverride?: 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;
/**
* 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.
* Returns undefined if no valid token is found.
*/
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;
6 changes: 6 additions & 0 deletions build/cli/src/utils/yml_utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ interface ActionYaml {
author: string;
inputs: Record<string, ActionInput>;
}
/**
* Resolves action.yml from the copilot package root, not cwd.
* When run as CLI from another repo, cwd is that repo; action.yml lives next to the bundle.
* - From source: __dirname is src/utils β†’ ../../action.yml = repo root.
* - From bundle (build/cli): __dirname is bundle dir β†’ ../../action.yml = package root.
*/
export declare function loadActionYaml(): ActionYaml;
export declare function getActionInputs(): Record<string, ActionInput>;
export declare function getActionInputsWithDefaults(): Record<string, string>;
Expand Down
Loading