Skip to content

Commit 459cbd6

Browse files
authored
Merge release/2.0.2 into master. Forced merge with PAT token.
Merge release/2.0.2 into master
2 parents a984194 + 5683c5a commit 459cbd6

29 files changed

+1522
-105
lines changed

build/cli/index.js

Lines changed: 166 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47252,6 +47252,7 @@ const dotenv = __importStar(__nccwpck_require__(2437));
4725247252
const local_action_1 = __nccwpck_require__(7002);
4725347253
const issue_repository_1 = __nccwpck_require__(57);
4725447254
const constants_1 = __nccwpck_require__(8593);
47255+
const setup_files_1 = __nccwpck_require__(1666);
4725547256
const logger_1 = __nccwpck_require__(8836);
4725647257
const prompts_1 = __nccwpck_require__(5554);
4725747258
const ai_1 = __nccwpck_require__(4470);
@@ -47650,12 +47651,27 @@ program
4765047651
process.exit(1);
4765147652
}
4765247653
(0, logger_1.logInfo)(`📦 Repository: ${gitInfo.owner}/${gitInfo.repo}`);
47654+
const token = (0, setup_files_1.getSetupToken)(cwd, options.token);
47655+
if (!token) {
47656+
(0, logger_1.logError)('🛑 Setup requires PERSONAL_ACCESS_TOKEN with a valid token.');
47657+
(0, logger_1.logInfo)(' You can:');
47658+
(0, logger_1.logInfo)(' • Pass it on the command line: copilot setup --token <your_github_token>');
47659+
(0, logger_1.logInfo)(' • Add it to your environment: export PERSONAL_ACCESS_TOKEN=your_github_token');
47660+
if ((0, setup_files_1.setupEnvFileExists)(cwd)) {
47661+
(0, logger_1.logInfo)(' • Or add PERSONAL_ACCESS_TOKEN=your_github_token to your existing .env file');
47662+
}
47663+
else {
47664+
(0, logger_1.logInfo)(' • Or create a .env file in this repo with: PERSONAL_ACCESS_TOKEN=your_github_token');
47665+
}
47666+
process.exit(1);
47667+
return;
47668+
}
4765347669
(0, logger_1.logInfo)('⚙️ Running initial setup (labels, issue types, access)...');
4765447670
const params = {
4765547671
[constants_1.INPUT_KEYS.DEBUG]: options.debug.toString(),
4765647672
[constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.INITIAL_SETUP,
4765747673
[constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: 1,
47658-
[constants_1.INPUT_KEYS.TOKEN]: options.token || process.env.PERSONAL_ACCESS_TOKEN,
47674+
[constants_1.INPUT_KEYS.TOKEN]: token,
4765947675
repo: {
4766047676
owner: gitInfo.owner,
4766147677
repo: gitInfo.repo,
@@ -48712,6 +48728,21 @@ class ProjectDetail {
4871248728
this.url = data[`url`] ?? '';
4871348729
this.number = data[`number`] ?? -1;
4871448730
}
48731+
/**
48732+
* Returns the full public URL to the project (board).
48733+
* Uses the URL from the API when present and valid; otherwise builds it from owner, type and number.
48734+
* Returns empty string when project number is invalid (e.g. missing from API).
48735+
*/
48736+
get publicUrl() {
48737+
if (this.url && typeof this.url === 'string' && this.url.startsWith('https://')) {
48738+
return this.url;
48739+
}
48740+
if (typeof this.number !== 'number' || this.number <= 0) {
48741+
return '';
48742+
}
48743+
const path = this.type === 'organization' ? 'orgs' : 'users';
48744+
return `https://github.com/${path}/${this.owner}/projects/${this.number}`;
48745+
}
4871548746
}
4871648747
exports.ProjectDetail = ProjectDetail;
4871748748

@@ -51556,14 +51587,22 @@ class ProjectRepository {
5155651587
number: issueOrPullRequestNumber
5155751588
});
5155851589
if (!issueOrPrResult.repository.issueOrPullRequest) {
51559-
console.error(`Issue or PR #${issueOrPullRequestNumber} not found.`);
51590+
(0, logger_1.logError)(`Issue or PR #${issueOrPullRequestNumber} not found in repository.`);
5156051591
return undefined;
5156151592
}
5156251593
const contentId = issueOrPrResult.repository.issueOrPullRequest.id;
5156351594
// Search for the item ID in the project with pagination
5156451595
let cursor = null;
5156551596
let projectItemId = undefined;
51597+
let totalItemsChecked = 0;
51598+
const maxPages = 100; // 100 * 100 = 10_000 items max to avoid runaway loops
51599+
let pageCount = 0;
5156651600
do {
51601+
if (pageCount >= maxPages) {
51602+
(0, logger_1.logError)(`Stopped after ${maxPages} pages (${totalItemsChecked} items). Issue or PR #${issueOrPullRequestNumber} not found in project.`);
51603+
break;
51604+
}
51605+
pageCount += 1;
5156751606
const projectQuery = `
5156851607
query($projectId: ID!, $cursor: String) {
5156951608
node(id: $projectId) {
@@ -51592,16 +51631,36 @@ class ProjectRepository {
5159251631
projectId: project.id,
5159351632
cursor
5159451633
});
51595-
const items = projectResult.node.items.nodes;
51634+
if (projectResult.node === null) {
51635+
(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.`);
51636+
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.`);
51637+
}
51638+
const items = projectResult.node.items?.nodes ?? [];
51639+
totalItemsChecked += items.length;
51640+
const pageInfo = projectResult.node.items?.pageInfo;
5159651641
const foundItem = items.find((item) => item.content?.id === contentId);
5159751642
if (foundItem) {
5159851643
projectItemId = foundItem.id;
5159951644
break;
5160051645
}
51601-
cursor = projectResult.node.items.pageInfo.hasNextPage
51602-
? projectResult.node.items.pageInfo.endCursor
51603-
: null;
51646+
// Advance cursor only when there is a next page AND a non-null cursor (avoid missing pages)
51647+
const hasNextPage = pageInfo?.hasNextPage === true;
51648+
const endCursor = pageInfo?.endCursor ?? null;
51649+
if (hasNextPage && endCursor) {
51650+
cursor = endCursor;
51651+
}
51652+
else {
51653+
if (hasNextPage && !endCursor) {
51654+
(0, logger_1.logError)(`Project items pagination: hasNextPage is true but endCursor is null (page ${pageCount}, ${totalItemsChecked} items so far). Cannot fetch more.`);
51655+
}
51656+
cursor = null;
51657+
}
5160451658
} while (cursor);
51659+
if (projectItemId === undefined) {
51660+
(0, logger_1.logError)(`Issue or PR #${issueOrPullRequestNumber} not found in project after checking ${totalItemsChecked} items (${pageCount} page(s)). ` +
51661+
`Link it to the project first, or wait for the board to sync.`);
51662+
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.`);
51663+
}
5160551664
return projectItemId;
5160651665
};
5160751666
this.isContentLinked = async (project, contentId, token) => {
@@ -54003,6 +54062,18 @@ class InitialSetupUseCase {
5400354062
(0, setup_files_1.ensureGitHubDirs)(process.cwd());
5400454063
const filesResult = (0, setup_files_1.copySetupFiles)(process.cwd());
5400554064
steps.push(`✅ Setup files: ${filesResult.copied} copied, ${filesResult.skipped} already existed`);
54065+
if (!(0, setup_files_1.hasValidSetupToken)(process.cwd())) {
54066+
(0, logger_1.logInfo)(' 🛑 Setup requires PERSONAL_ACCESS_TOKEN (environment or .env) with a valid token.');
54067+
errors.push('PERSONAL_ACCESS_TOKEN must be set (environment or .env) with a valid token to run setup.');
54068+
results.push(new result_1.Result({
54069+
id: this.taskId,
54070+
success: false,
54071+
executed: true,
54072+
steps: steps,
54073+
errors: errors,
54074+
}));
54075+
return results;
54076+
}
5400654077
// 1. Verificar acceso a GitHub con Personal Access Token
5400754078
(0, logger_1.logInfo)('🔐 Checking GitHub access...');
5400854079
const githubAccessResult = await this.verifyGitHubAccess(param);
@@ -57923,7 +57994,7 @@ class CheckPriorityIssueSizeUseCase {
5792357994
success: true,
5792457995
executed: true,
5792557996
steps: [
57926-
`Priority set to \`${priorityLabel}\` in [${project.title}](https://github.com/${param.owner}/${param.repo}/projects/${project.id}).`,
57997+
`Priority set to \`${priorityLabel}\` in [${project.title}](${project.publicUrl}).`,
5792757998
],
5792857999
}));
5792958000
}
@@ -58364,7 +58435,7 @@ class MoveIssueToInProgressUseCase {
5836458435
success: true,
5836558436
executed: true,
5836658437
steps: [
58367-
`Moved issue to \`${columnName}\` in [${project.title}](https://github.com/${param.owner}/${param.repo}/projects/${project.id}).`,
58438+
`Moved issue to \`${columnName}\` in [${project.title}](${project.publicUrl}).`,
5836858439
],
5836958440
}));
5837058441
}
@@ -59116,7 +59187,7 @@ class CheckPriorityPullRequestSizeUseCase {
5911659187
success: true,
5911759188
executed: true,
5911859189
steps: [
59119-
`Priority set to \`${priorityLabel}\` in [${project.title}](https://github.com/${param.owner}/${param.repo}/projects/${project.id}).`,
59190+
`Priority set to \`${priorityLabel}\` in [${project.title}](${project.publicUrl}).`,
5912059191
],
5912159192
}));
5912259193
}
@@ -60342,6 +60413,10 @@ var __importStar = (this && this.__importStar) || (function () {
6034260413
Object.defineProperty(exports, "__esModule", ({ value: true }));
6034360414
exports.ensureGitHubDirs = ensureGitHubDirs;
6034460415
exports.copySetupFiles = copySetupFiles;
60416+
exports.ensureEnvWithToken = ensureEnvWithToken;
60417+
exports.getSetupToken = getSetupToken;
60418+
exports.hasValidSetupToken = hasValidSetupToken;
60419+
exports.setupEnvFileExists = setupEnvFileExists;
6034560420
const fs = __importStar(__nccwpck_require__(7147));
6034660421
const path = __importStar(__nccwpck_require__(1017));
6034760422
const logger_1 = __nccwpck_require__(8836);
@@ -60370,11 +60445,13 @@ function ensureGitHubDirs(cwd) {
6037060445
* Copy setup files from setup/ to repo (.github/ workflows, ISSUE_TEMPLATE, pull_request_template.md, .env at root).
6037160446
* Skips files that already exist at destination (no overwrite).
6037260447
* Logs each file copied or skipped. No-op if setup/ does not exist.
60373-
* @param cwd - Repo root
60448+
* By default setup dir is the copilot package root (not cwd), so it works when running from another repo.
60449+
* @param cwd - Repo root (destination)
60450+
* @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root.
6037460451
* @returns { copied, skipped }
6037560452
*/
60376-
function copySetupFiles(cwd) {
60377-
const setupDir = path.join(cwd, 'setup');
60453+
function copySetupFiles(cwd, setupDirOverride) {
60454+
const setupDir = setupDirOverride ?? path.join(__dirname, '..', '..', 'setup');
6037860455
if (!fs.existsSync(setupDir))
6037960456
return { copied: 0, skipped: 0 };
6038060457
let copied = 0;
@@ -60430,20 +60507,80 @@ function copySetupFiles(cwd) {
6043060507
copied += 1;
6043160508
}
6043260509
}
60433-
const envSrc = path.join(setupDir, '.env');
60434-
const envDst = path.join(cwd, '.env');
60435-
if (fs.existsSync(envSrc) && fs.statSync(envSrc).isFile()) {
60436-
if (fs.existsSync(envDst)) {
60437-
(0, logger_1.logInfo)(' ⏭️ .env already exists; skipping.');
60438-
skipped += 1;
60510+
ensureEnvWithToken(cwd);
60511+
return { copied, skipped };
60512+
}
60513+
const ENV_TOKEN_KEY = 'PERSONAL_ACCESS_TOKEN';
60514+
const ENV_PLACEHOLDER_VALUE = 'github_pat_11..';
60515+
/** Minimum length for a token to be considered "defined" (not placeholder). */
60516+
const MIN_VALID_TOKEN_LENGTH = 20;
60517+
function getTokenFromEnvFile(envPath) {
60518+
if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile())
60519+
return null;
60520+
const content = fs.readFileSync(envPath, 'utf8');
60521+
const match = content.match(new RegExp(`^${ENV_TOKEN_KEY}=(.+)$`, 'm'));
60522+
if (!match)
60523+
return null;
60524+
const value = match[1].trim().replace(/^["']|["']$/g, '');
60525+
return value.length > 0 ? value : null;
60526+
}
60527+
/**
60528+
* Logs the current state of PERSONAL_ACCESS_TOKEN (environment or .env). Does not create .env.
60529+
*/
60530+
function ensureEnvWithToken(cwd) {
60531+
const envPath = path.join(cwd, '.env');
60532+
const tokenInEnv = process.env[ENV_TOKEN_KEY]?.trim();
60533+
if (tokenInEnv) {
60534+
(0, logger_1.logInfo)(' 🔑 PERSONAL_ACCESS_TOKEN is set in environment; .env not needed.');
60535+
return;
60536+
}
60537+
if (fs.existsSync(envPath)) {
60538+
const tokenInFile = getTokenFromEnvFile(envPath);
60539+
if (tokenInFile) {
60540+
(0, logger_1.logInfo)(' ✅ .env exists and contains PERSONAL_ACCESS_TOKEN.');
6043960541
}
6044060542
else {
60441-
fs.copyFileSync(envSrc, envDst);
60442-
(0, logger_1.logInfo)(' ✅ Copied setup/.env → .env');
60443-
copied += 1;
60543+
(0, logger_1.logInfo)(' ⚠️ .env exists but PERSONAL_ACCESS_TOKEN is missing or empty.');
6044460544
}
60545+
return;
6044560546
}
60446-
return { copied, skipped };
60547+
(0, logger_1.logInfo)(' 💡 You can create a .env file here with PERSONAL_ACCESS_TOKEN=your_token or set it in your environment.');
60548+
}
60549+
function isTokenValueValid(token) {
60550+
const t = token.trim();
60551+
return t.length >= MIN_VALID_TOKEN_LENGTH && t !== ENV_PLACEHOLDER_VALUE;
60552+
}
60553+
/**
60554+
* Resolves the PERSONAL_ACCESS_TOKEN for setup from a single priority order:
60555+
* 1. override (e.g. CLI --token) if provided and valid,
60556+
* 2. process.env.PERSONAL_ACCESS_TOKEN,
60557+
* 3. .env file in cwd.
60558+
* Returns undefined if no valid token is found.
60559+
*/
60560+
function getSetupToken(cwd, override) {
60561+
const overrideTrimmed = override?.trim();
60562+
if (overrideTrimmed && isTokenValueValid(overrideTrimmed))
60563+
return overrideTrimmed;
60564+
const fromEnv = process.env[ENV_TOKEN_KEY]?.trim();
60565+
if (fromEnv && isTokenValueValid(fromEnv))
60566+
return fromEnv;
60567+
const envPath = path.join(cwd, '.env');
60568+
const fromFile = getTokenFromEnvFile(envPath);
60569+
if (fromFile !== null && isTokenValueValid(fromFile))
60570+
return fromFile;
60571+
return undefined;
60572+
}
60573+
/**
60574+
* Returns true if a valid setup token is available (same resolution order as getSetupToken).
60575+
* Pass an optional override (e.g. CLI --token) so validation considers all sources consistently.
60576+
*/
60577+
function hasValidSetupToken(cwd, override) {
60578+
return getSetupToken(cwd, override) !== undefined;
60579+
}
60580+
/** Returns true if a .env file exists in the given directory. */
60581+
function setupEnvFileExists(cwd) {
60582+
const envPath = path.join(cwd, '.env');
60583+
return fs.existsSync(envPath) && fs.statSync(envPath).isFile();
6044760584
}
6044860585

6044960586

@@ -60650,8 +60787,14 @@ exports.getActionInputsWithDefaults = getActionInputsWithDefaults;
6065060787
const fs = __importStar(__nccwpck_require__(7147));
6065160788
const path = __importStar(__nccwpck_require__(1017));
6065260789
const yaml = __importStar(__nccwpck_require__(1917));
60790+
/**
60791+
* Resolves action.yml from the copilot package root, not cwd.
60792+
* When run as CLI from another repo, cwd is that repo; action.yml lives next to the bundle.
60793+
* - From source: __dirname is src/utils → ../../action.yml = repo root.
60794+
* - From bundle (build/cli): __dirname is bundle dir → ../../action.yml = package root.
60795+
*/
6065360796
function loadActionYaml() {
60654-
const actionYamlPath = path.join(process.cwd(), 'action.yml');
60797+
const actionYamlPath = path.join(__dirname, '..', '..', 'action.yml');
6065560798
const yamlContent = fs.readFileSync(actionYamlPath, 'utf8');
6065660799
return yaml.load(yamlContent);
6065760800
}

build/cli/src/data/model/project_detail.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ export declare class ProjectDetail {
66
url: string;
77
number: number;
88
constructor(data: any);
9+
/**
10+
* Returns the full public URL to the project (board).
11+
* Uses the URL from the API when present and valid; otherwise builds it from owner, type and number.
12+
* Returns empty string when project number is invalid (e.g. missing from API).
13+
*/
14+
get publicUrl(): string;
915
}

build/cli/src/utils/setup_files.d.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,31 @@ export declare function ensureGitHubDirs(cwd: string): void;
77
* Copy setup files from setup/ to repo (.github/ workflows, ISSUE_TEMPLATE, pull_request_template.md, .env at root).
88
* Skips files that already exist at destination (no overwrite).
99
* Logs each file copied or skipped. No-op if setup/ does not exist.
10-
* @param cwd - Repo root
10+
* By default setup dir is the copilot package root (not cwd), so it works when running from another repo.
11+
* @param cwd - Repo root (destination)
12+
* @param setupDirOverride - Optional path to setup/ folder (for tests). If not set, uses package root.
1113
* @returns { copied, skipped }
1214
*/
13-
export declare function copySetupFiles(cwd: string): {
15+
export declare function copySetupFiles(cwd: string, setupDirOverride?: string): {
1416
copied: number;
1517
skipped: number;
1618
};
19+
/**
20+
* Logs the current state of PERSONAL_ACCESS_TOKEN (environment or .env). Does not create .env.
21+
*/
22+
export declare function ensureEnvWithToken(cwd: string): void;
23+
/**
24+
* Resolves the PERSONAL_ACCESS_TOKEN for setup from a single priority order:
25+
* 1. override (e.g. CLI --token) if provided and valid,
26+
* 2. process.env.PERSONAL_ACCESS_TOKEN,
27+
* 3. .env file in cwd.
28+
* Returns undefined if no valid token is found.
29+
*/
30+
export declare function getSetupToken(cwd: string, override?: string): string | undefined;
31+
/**
32+
* Returns true if a valid setup token is available (same resolution order as getSetupToken).
33+
* Pass an optional override (e.g. CLI --token) so validation considers all sources consistently.
34+
*/
35+
export declare function hasValidSetupToken(cwd: string, override?: string): boolean;
36+
/** Returns true if a .env file exists in the given directory. */
37+
export declare function setupEnvFileExists(cwd: string): boolean;

build/cli/src/utils/yml_utils.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ interface ActionYaml {
88
author: string;
99
inputs: Record<string, ActionInput>;
1010
}
11+
/**
12+
* Resolves action.yml from the copilot package root, not cwd.
13+
* When run as CLI from another repo, cwd is that repo; action.yml lives next to the bundle.
14+
* - From source: __dirname is src/utils → ../../action.yml = repo root.
15+
* - From bundle (build/cli): __dirname is bundle dir → ../../action.yml = package root.
16+
*/
1117
export declare function loadActionYaml(): ActionYaml;
1218
export declare function getActionInputs(): Record<string, ActionInput>;
1319
export declare function getActionInputsWithDefaults(): Record<string, string>;

0 commit comments

Comments
 (0)