@@ -11532,26 +11532,31 @@ var __importStar = (this && this.__importStar) || (function () {
1153211532 return result;
1153311533 };
1153411534})();
11535+ var __importDefault = (this && this.__importDefault) || function (mod) {
11536+ return (mod && mod.__esModule) ? mod : { "default": mod };
11537+ };
1153511538Object.defineProperty(exports, "__esModule", ({ value: true }));
1153611539const core = __importStar(__nccwpck_require__(2186));
11540+ const CONST_1 = __importDefault(__nccwpck_require__(9873));
1153711541const DeployChecklistUtils_1 = __nccwpck_require__(2141);
1153811542const run = function () {
1153911543 return (0, DeployChecklistUtils_1.getDeployChecklist)()
1154011544 .then(({ labels, number }) => {
11541- const labelsNames = labels.map((label) => {
11542- if (typeof label === 'string') {
11543- return '';
11544- }
11545- return label.name;
11546- });
11547- console.log(`Found deploy checklist with labels: ${JSON.stringify(labelsNames)}`);
11548- core.setOutput('IS_LOCKED', labelsNames.includes('🔐 LockCashDeploys 🔐'));
11545+ const labelNames = (labels ?? []).map((label) => (typeof label === 'string' ? label : (label.name ?? '')));
11546+ const isLocked = labelNames.includes(CONST_1.default.LABELS.LOCK_DEPLOY);
11547+ console.log(`Found deploy checklist #${number} with labels: ${JSON.stringify(labelNames)}`);
11548+ core.setOutput('IS_LOCKED', isLocked);
1154911549 core.setOutput('NUMBER', number);
1155011550 })
11551- .catch((err) => {
11552- console.warn('No open deploy checklist found, continuing...', err);
11553- core.setOutput('IS_LOCKED', false);
11554- core.setOutput('NUMBER', 0);
11551+ .catch((error) => {
11552+ if (error instanceof DeployChecklistUtils_1.NoOpenDeployChecklistError) {
11553+ console.log(`No open deploy checklist; treating as not locked. ${error.message}`);
11554+ core.setOutput('IS_LOCKED', false);
11555+ core.setOutput('NUMBER', 0);
11556+ return;
11557+ }
11558+ const message = error instanceof Error ? error.message : String(error);
11559+ core.setFailed(`Could not resolve deploy checklist; blocking deploy: ${message}`);
1155511560 });
1155611561};
1155711562if (require.main === require.cache[eval('__filename')]) {
@@ -11650,13 +11655,50 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
1165011655 return (mod && mod.__esModule) ? mod : { "default": mod };
1165111656};
1165211657Object.defineProperty(exports, "__esModule", ({ value: true }));
11658+ exports.NoOpenDeployChecklistError = void 0;
1165311659exports.getDeployChecklist = getDeployChecklist;
1165411660exports.getDeployChecklistData = getDeployChecklistData;
1165511661exports.generateDeployChecklistBodyAndAssignees = generateDeployChecklistBodyAndAssignees;
1165611662exports.parseChecklistSection = parseChecklistSection;
1165711663const dedent_1 = __importDefault(__nccwpck_require__(6762));
1165811664const CONST_1 = __importDefault(__nccwpck_require__(9873));
1165911665const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
11666+ /** Milliseconds to wait before each subsequent `listForRepo` attempt. */
11667+ const LIST_RETRY_DELAYS_MS = [2000, 5000];
11668+ /**
11669+ * HTTP statuses that indicate a definitively-permanent failure of `listForRepo` -
11670+ * retrying cannot help (auth, missing resource, validation). Note that `403` is
11671+ * intentionally absent because GitHub returns secondary-rate-limit and abuse
11672+ * detection responses as `403` and those should be retried with backoff.
11673+ */
11674+ const NON_RETRYABLE_LIST_STATUSES = new Set([401, 404, 422]);
11675+ /**
11676+ * Duck-type check for "this error is a permanent failure from `listForRepo`".
11677+ * We avoid `instanceof RequestError` because the bundled action ends up with
11678+ * multiple copies of that class (one from `@octokit/request-error` directly,
11679+ * one nested via `@actions/github` -> `@octokit/core`) and `instanceof`
11680+ * compares identity, so it can miss real errors thrown by octokit.
11681+ */
11682+ function isPermanentListError(error) {
11683+ if (typeof error !== 'object' || error === null || !('status' in error)) {
11684+ return false;
11685+ }
11686+ const status = error.status;
11687+ return typeof status === 'number' && NON_RETRYABLE_LIST_STATUSES.has(status);
11688+ }
11689+ /**
11690+ * Thrown by `getDeployChecklist` when GitHub successfully confirms there is no open
11691+ * StagingDeployCash issue and the most recent checklist is closed - i.e. we're in the
11692+ * legitimate window between deploy cycles. Callers that need to distinguish "benign
11693+ * empty" from "could not resolve" should catch this specific subclass.
11694+ */
11695+ class NoOpenDeployChecklistError extends Error {
11696+ constructor(message) {
11697+ super(message);
11698+ this.name = 'NoOpenDeployChecklistError';
11699+ }
11700+ }
11701+ exports.NoOpenDeployChecklistError = NoOpenDeployChecklistError;
1166011702/**
1166111703 * Generic checklist section parser. Extracts a section from the issue body,
1166211704 * parses checkbox items within it, and returns ChecklistItems sorted by number.
@@ -11692,24 +11734,75 @@ function getDeployChecklistDeployBlockers(issue) {
1169211734function getDeployChecklistInternalQA(issue) {
1169311735 return parseChecklistSection(issue.body, /Internal QA:\*\*\r?\n((?:- \[[ x]].*\r?\n)+)/, new RegExp(`- \\[([ x])]\\s(${CONST_1.default.PULL_REQUEST_REGEX.source})`, 'g'), (url) => url.split('-').at(0)?.trim() ?? '');
1169411736}
11737+ /**
11738+ * Calls `issues.listForRepo` with simple retry-on-throw. Retries 1 + `LIST_RETRY_DELAYS_MS.length`
11739+ * times total, sleeping the corresponding delay between attempts. Empty results are NOT retried -
11740+ * the caller must decide whether an empty list is legitimate. Permanent statuses listed in
11741+ * `NON_RETRYABLE_LIST_STATUSES` short-circuit and re-throw on the first attempt.
11742+ */
11743+ async function listForRepoWithRetry(params) {
11744+ let lastError;
11745+ const maxAttempts = LIST_RETRY_DELAYS_MS.length + 1;
11746+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
11747+ try {
11748+ const { data } = await GithubUtils_1.default.octokit.issues.listForRepo(params);
11749+ return data;
11750+ }
11751+ catch (error) {
11752+ lastError = error;
11753+ if (isPermanentListError(error)) {
11754+ console.warn(`listForRepo failed with permanent status ${error.status}; not retrying`, error);
11755+ throw error;
11756+ }
11757+ const delay = LIST_RETRY_DELAYS_MS.at(attempt - 1);
11758+ if (delay === undefined) {
11759+ throw error;
11760+ }
11761+ console.warn(`listForRepo attempt ${attempt}/${maxAttempts} failed; retrying in ${delay}ms`, error);
11762+ await new Promise((resolve) => {
11763+ setTimeout(resolve, delay);
11764+ });
11765+ }
11766+ }
11767+ throw lastError;
11768+ }
1169511769async function getDeployChecklist() {
11696- const { data } = await GithubUtils_1.default.octokit.issues.listForRepo ({
11770+ const openIssues = await listForRepoWithRetry ({
1169711771 owner: CONST_1.default.GITHUB_OWNER,
1169811772 repo: CONST_1.default.APP_REPO,
1169911773 labels: CONST_1.default.LABELS.STAGING_DEPLOY,
1170011774 state: 'open',
1170111775 });
11702- if (!data.length) {
11703- throw new Error(`Unable to find ${CONST_1.default.LABELS.STAGING_DEPLOY} issue.`);
11776+ if (openIssues.length > 1) {
11777+ throw new Error(`Found more than one open ${CONST_1.default.LABELS.STAGING_DEPLOY} issue: #${openIssues.map((issue) => issue.number).join(', #')}.`);
11778+ }
11779+ if (openIssues.length === 1) {
11780+ const issue = openIssues.at(0);
11781+ if (!issue) {
11782+ throw new Error(`Found an undefined ${CONST_1.default.LABELS.STAGING_DEPLOY} issue.`);
11783+ }
11784+ return getDeployChecklistData(issue);
1170411785 }
11705- if (data.length > 1) {
11706- throw new Error(`Found more than one ${CONST_1.default.LABELS.STAGING_DEPLOY} issue.`);
11786+ // The filtered open list was empty. Cross-check against state:'all' to tell
11787+ // a legitimate between-cycles window apart from an API inconsistency. Any open
11788+ // issue anywhere in the response - not just the most recent - blocks the deploy,
11789+ // so a stale older issue that got reopened cannot slip through.
11790+ const allIssues = await listForRepoWithRetry({
11791+ owner: CONST_1.default.GITHUB_OWNER,
11792+ repo: CONST_1.default.APP_REPO,
11793+ labels: CONST_1.default.LABELS.STAGING_DEPLOY,
11794+ state: 'all',
11795+ });
11796+ if (allIssues.length === 0) {
11797+ // The label has been in continuous use; an empty state:'all' result is pathological.
11798+ throw new Error(`No ${CONST_1.default.LABELS.STAGING_DEPLOY} issues found at all (state:'all' returned empty). Refusing to deploy.`);
1170711799 }
11708- const issue = data.at(0 );
11709- if (!issue ) {
11710- throw new Error(`Found an undefined ${CONST_1.default.LABELS.STAGING_DEPLOY} issue.`);
11800+ const openIssuesInAll = allIssues.filter((issue) => issue.state === 'open' );
11801+ if (openIssuesInAll.length > 0 ) {
11802+ throw new Error(`Inconsistent GitHub response: state:open returned empty but state:all reports open ${CONST_1.default.LABELS.STAGING_DEPLOY} issue(s) #${openIssuesInAll.map((issue) => issue.number).join(', #')}. Refusing to deploy .`);
1171111803 }
11712- return getDeployChecklistData(issue);
11804+ const mostRecent = allIssues.at(0);
11805+ throw new NoOpenDeployChecklistError(`No open ${CONST_1.default.LABELS.STAGING_DEPLOY} issue (most recent #${mostRecent?.number} is closed).`);
1171311806}
1171411807function getDeployChecklistData(issue) {
1171511808 try {
0 commit comments