@firestartr/cli 2.4.0-snapshot-2 → 2.4.0-snapshot-4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -284813,6 +284813,27 @@ async function addCommitStatus(state, sha, repo, owner = 'prefapp', target_url =
284813
284813
  context,
284814
284814
  });
284815
284815
  }
284816
+ async function tryCreateRef(ref, sha, repo, owner = 'prefapp') {
284817
+ github_src_logger.info(`Creating ref ${ref} at ${sha} in ${owner}/${repo}`);
284818
+ const octokit = await getOctokitForOrg(owner);
284819
+ const normalizedRef = ref.startsWith('refs/') ? ref : `refs/${ref}`;
284820
+ try {
284821
+ await octokit.rest.git.createRef({
284822
+ owner,
284823
+ repo,
284824
+ ref: normalizedRef,
284825
+ sha,
284826
+ });
284827
+ return true;
284828
+ }
284829
+ catch (e) {
284830
+ const message = e?.message || '';
284831
+ if (e?.status === 422 && message.includes('Reference already exists')) {
284832
+ return false;
284833
+ }
284834
+ throw e;
284835
+ }
284836
+ }
284816
284837
  async function getRepoIssuesLabels(owner, repo) {
284817
284838
  github_src_logger.info(`Getting issues labels for ${owner}/${repo}`);
284818
284839
  const octokit = await getOctokitForOrg(owner);
@@ -284843,6 +284864,7 @@ async function getRepoIssuesLabels(owner, repo) {
284843
284864
  getOIDCRepo,
284844
284865
  addStatusCheck,
284845
284866
  addCommitStatus,
284867
+ tryCreateRef,
284846
284868
  getRepoIssuesLabels,
284847
284869
  });
284848
284870
 
@@ -284861,8 +284883,22 @@ async function getTeamMembers(team, org) {
284861
284883
  async function getTeamInfo(team, org) {
284862
284884
  github_src_logger.info(`Getting info for ${org}/${team}`);
284863
284885
  const octokit = await getOctokitForOrg(org);
284864
- const res = await octokit.rest.teams.getByName({ org: org, team_slug: team });
284865
- return res['data'];
284886
+ try {
284887
+ const res = await octokit.rest.teams.getByName({
284888
+ org: org,
284889
+ team_slug: team,
284890
+ });
284891
+ return res['data'];
284892
+ }
284893
+ catch (err) {
284894
+ const status = err?.status || err?.response?.status;
284895
+ const message = err instanceof Error ? err.message : String(err);
284896
+ const error = new Error(`Error getting GitHub team "${team}" in org "${org}": ${message}`);
284897
+ if (status) {
284898
+ error.status = status;
284899
+ }
284900
+ throw error;
284901
+ }
284866
284902
  }
284867
284903
  async function getTeamRoleUser(org, team, username) {
284868
284904
  github_src_logger.info(`Getting role for ${username} in ${org}/${team}`);
@@ -285007,6 +285043,36 @@ async function readMultiPartStickyIdsFromPrBody(octokit, owner, repo, pr, baseKi
285007
285043
  }
285008
285044
  return ids;
285009
285045
  }
285046
+ async function readMultiPartStickyIdsFromComments(octokit, owner, repo, pr, baseKind) {
285047
+ const ids = new Map();
285048
+ let page = 1;
285049
+ while (true) {
285050
+ const resp = await octokit.rest.issues.listComments({
285051
+ owner,
285052
+ repo,
285053
+ issue_number: pr,
285054
+ per_page: 100,
285055
+ page,
285056
+ });
285057
+ for (const comment of resp.data) {
285058
+ const body = comment.body ?? '';
285059
+ for (let i = 0; i < MAX_MULTIPART_COMMENTS; i++) {
285060
+ if (body.includes(multiPartBodyMarker(baseKind, i))) {
285061
+ ids.set(i, comment.id);
285062
+ }
285063
+ }
285064
+ }
285065
+ if (resp.data.length < 100)
285066
+ return ids;
285067
+ page++;
285068
+ }
285069
+ }
285070
+ async function readMultiPartStickyIds(octokit, owner, repo, pr, baseKind) {
285071
+ const ids = await readMultiPartStickyIdsFromPrBody(octokit, owner, repo, pr, baseKind);
285072
+ if (ids.size > 0)
285073
+ return ids;
285074
+ return readMultiPartStickyIdsFromComments(octokit, owner, repo, pr, baseKind);
285075
+ }
285010
285076
  async function writeMultiPartStickyIdsToPrBody(octokit, owner, repo, pr, baseKind, ids) {
285011
285077
  const oldBody = await getPrBody(octokit, owner, repo, pr);
285012
285078
  let body = oldBody;
@@ -285049,7 +285115,7 @@ async function upsertMultiPartStickyComments(octokit, params) {
285049
285115
  const totalParts = bodies.length;
285050
285116
  await withLock(lockKey, async () => {
285051
285117
  // Read existing comment IDs for this base kind
285052
- const existingIds = await readMultiPartStickyIdsFromPrBody(octokit, owner, repo, pullNumber, baseKind);
285118
+ const existingIds = await readMultiPartStickyIds(octokit, owner, repo, pullNumber, baseKind);
285053
285119
  const newIds = new Map();
285054
285120
  // Process each part of the new content
285055
285121
  for (let partIndex = 0; partIndex < totalParts; partIndex++) {
@@ -285138,6 +285204,11 @@ async function getPrMergeCommitSHA(pull_number, repo, owner = 'prefapp') {
285138
285204
  }
285139
285205
  throw new Error(`No merge_commit_sha value in prData.data: ${prData.data}`);
285140
285206
  }
285207
+ async function getPrBaseSHA(pull_number, repo, owner = 'prefapp') {
285208
+ github_src_logger.info(`Getting base SHA for PR ${pull_number} of ${owner}/${repo}`);
285209
+ const prData = await getPrData(pull_number, repo, owner);
285210
+ return prData.data.base.sha;
285211
+ }
285141
285212
  /*
285142
285213
  * Receives a comment, splits it into commentMaxSize sized chunks and returns
285143
285214
  * the resulting list
@@ -285171,11 +285242,22 @@ function divideCommentIntoChunks(comment, sizeReduction = 0) {
285171
285242
  async function getPrFiles(pr_number, repo, owner = 'prefapp') {
285172
285243
  github_src_logger.info(`Getting PR details of PR ${pr_number} of ${owner}/${repo}`);
285173
285244
  const octokit = await getOctokitForOrg(owner);
285174
- return await octokit.rest.pulls.listFiles({
285175
- owner,
285176
- repo,
285177
- pull_number: pr_number,
285178
- });
285245
+ const data = [];
285246
+ let page = 1;
285247
+ while (true) {
285248
+ const resp = await octokit.rest.pulls.listFiles({
285249
+ owner,
285250
+ repo,
285251
+ pull_number: pr_number,
285252
+ per_page: 100,
285253
+ page,
285254
+ });
285255
+ data.push(...resp.data);
285256
+ if (resp.data.length < 100) {
285257
+ return { ...resp, data };
285258
+ }
285259
+ page++;
285260
+ }
285179
285261
  }
285180
285262
  async function filterPrBy(filter, opts) {
285181
285263
  let foundPr = null;
@@ -285203,6 +285285,7 @@ async function filterPrBy(filter, opts) {
285203
285285
  commentInPR,
285204
285286
  getPrLastCommitSHA,
285205
285287
  getPrMergeCommitSHA,
285288
+ getPrBaseSHA,
285206
285289
  divideCommentIntoChunks,
285207
285290
  getPrFiles,
285208
285291
  filterPrBy,
@@ -286585,7 +286668,11 @@ class GlobalDefault extends DefaultSection {
286585
286668
  // Thus it performs a nullify of the designed patches
286586
286669
 
286587
286670
 
286588
- const ONE_WAY_DEFINITIONS = ['/spec/vars', '/spec/actions/oidc', '/spec/repo/labels'];
286671
+ const ONE_WAY_DEFINITIONS = [
286672
+ '/spec/vars',
286673
+ '/spec/actions/oidc',
286674
+ '/spec/repo/labels',
286675
+ ];
286589
286676
  function applyOneWayDefs(crSpecs) {
286590
286677
  const crSpecsClone = JSON.parse(JSON.stringify(crSpecs));
286591
286678
  for (const defPath of ONE_WAY_DEFINITIONS) {
@@ -293687,7 +293774,6 @@ function toJson_FirestartrGithubRepositorySpecRepo(obj) {
293687
293774
  'allowUpdateBranch': obj.allowUpdateBranch,
293688
293775
  'hasIssues': obj.hasIssues,
293689
293776
  'hasWiki': obj.hasWiki,
293690
- 'pages': obj.pages,
293691
293777
  'topics': obj.topics?.map(y => y),
293692
293778
  'labels': obj.labels?.map(y => toJson_FirestartrGithubRepositorySpecRepoLabels(y)),
293693
293779
  'visibility': obj.visibility,
@@ -297088,6 +297174,32 @@ function searchSecretKey(secretClaim, key) {
297088
297174
  return found;
297089
297175
  }
297090
297176
 
297177
+ ;// CONCATENATED MODULE: ../cdk8s_renderer/src/validations/componentPagesPath.ts
297178
+ /**
297179
+ * Throws if any ComponentClaim's providers.github.pages.path is set to any value other than '/' or '/docs'.
297180
+ * Should be called as a final render-time validation (never mutates input).
297181
+ */
297182
+ function validateComponentPagesPath(renderClaims) {
297183
+ for (const ref of Object.keys(renderClaims)) {
297184
+ const claimEntry = renderClaims[ref];
297185
+ // Heuristic: claim.kind === 'ComponentClaim' (may need to adjust based on naming)
297186
+ if (claimEntry.claim &&
297187
+ claimEntry.claim.kind === 'ComponentClaim' &&
297188
+ claimEntry.claim.providers &&
297189
+ claimEntry.claim.providers.github &&
297190
+ claimEntry.claim.providers.github.pages &&
297191
+ typeof claimEntry.claim.providers.github.pages === 'object') {
297192
+ const pages = claimEntry.claim.providers.github.pages;
297193
+ if (Object.prototype.hasOwnProperty.call(pages.source, 'path') &&
297194
+ typeof pages.source.path === 'string' &&
297195
+ pages.source.path !== '/' &&
297196
+ pages.source.path !== '/docs') {
297197
+ throw new Error(`ComponentClaim/${claimEntry.claim.name}: providers.github.pages.source.path must be '/' or '/docs' if set. Found: '${pages.source.path}'`);
297198
+ }
297199
+ }
297200
+ }
297201
+ }
297202
+
297091
297203
  ;// CONCATENATED MODULE: ../cdk8s_renderer/src/renderer/renderer.ts
297092
297204
 
297093
297205
 
@@ -297096,6 +297208,7 @@ function searchSecretKey(secretClaim, key) {
297096
297208
 
297097
297209
 
297098
297210
 
297211
+
297099
297212
  /*
297100
297213
  * Function called when rendering but not importing
297101
297214
  *
@@ -297112,6 +297225,7 @@ async function renderer_render(catalogScope, firestartrScope, claimList) {
297112
297225
  const result = await renderClaims(catalogScope, firestartrScope, data);
297113
297226
  try {
297114
297227
  validateSubReferences(data.renderClaims);
297228
+ validateComponentPagesPath(data.renderClaims);
297115
297229
  validateTfStateKeyUniqueness(result);
297116
297230
  validateCrSizes(result);
297117
297231
  validatePermissionsUniqueness(result);
@@ -298206,15 +298320,20 @@ class RepoGithubDecanter extends GithubDecanter {
298206
298320
  });
298207
298321
  }
298208
298322
  __decantPages() {
298209
- if (this.data.repoDetails.has_pages) {
298210
- this.__patchClaim({
298211
- op: 'add',
298212
- value: {
298213
- cname: this.data.pages.cname,
298214
- source: this.data.pages.source,
298215
- },
298216
- path: '/providers/github/pages',
298217
- });
298323
+ if (this.data.pages) {
298324
+ if (this.data.pages.build_type === 'workflow') {
298325
+ importer_src_logger.info(`Repository ${this.data.repoDetails.name} is using GitHub Actions for Pages deployment, skipping Pages configuration in claim as it's managed via workflow.`);
298326
+ }
298327
+ else if (this.data.pages.build_type === 'legacy') {
298328
+ this.__patchClaim({
298329
+ op: 'add',
298330
+ path: '/providers/github/pages',
298331
+ value: {
298332
+ cname: this.data.pages.cname || '',
298333
+ source: this.data.pages.source,
298334
+ },
298335
+ });
298336
+ }
298218
298337
  }
298219
298338
  }
298220
298339
  __decantOIDC() {
@@ -300614,8 +300733,9 @@ async function resolveDep(namespace, resolve, deps, references, getItemByItemPat
300614
300733
  for (const nestedRef of resolver_walk(ref)) {
300615
300734
  references.push(nestedRef);
300616
300735
  }
300617
- const secret = resolve.apiGroup === catalog_common.types.controller.FirestartrApiGroup ||
300618
- resolve.apiGroup === catalog_common.types.controller.ExternalSecretsApiGroup
300736
+ const secret = resolve.needsSecret !== false &&
300737
+ (resolve.apiGroup === catalog_common.types.controller.FirestartrApiGroup ||
300738
+ resolve.apiGroup === catalog_common.types.controller.ExternalSecretsApiGroup)
300619
300739
  ? await resolveSecretRef(namespace, ref, getSecret)
300620
300740
  : undefined;
300621
300741
  deps[kr] = {
@@ -303757,18 +303877,24 @@ ${message}
303757
303877
  `;
303758
303878
  await github.pulls.commentInPR(comment, prNumber, repo, org, 'logs');
303759
303879
  }
303760
- async function publishPlan(item, planOutput, prNumber, repo, org, isSuccess = true) {
303880
+ async function publishPlan(item, planOutput, prNumber, repo, org, isSuccess = true, operation = 'plan', source = '') {
303761
303881
  try {
303882
+ const kind = item.kind || 'UnknownKind';
303883
+ const name = item.metadata?.name || item.metadata?.generateName || 'unknown';
303762
303884
  const dividedOutput = github.pulls.divideCommentIntoChunks(planOutput, 250);
303763
303885
  const statusEmoji = isSuccess ? '✅' : '❌';
303764
303886
  const statusText = isSuccess ? 'Succeeded' : 'Failed';
303887
+ const operationTitle = operation
303888
+ .split('-')
303889
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
303890
+ .join('-');
303765
303891
  const commentBodies = dividedOutput.map((commentContent, index) => {
303766
303892
  const isMultiPart = dividedOutput.length > 1;
303767
303893
  const partIndicator = isMultiPart ? ` (Part ${index + 1})` : '';
303768
303894
  return `<h1>
303769
- <img width="25" src="https://raw.githubusercontent.com/firestartr-pro/docs/refs/heads/main/logos/square-nobg.png"> Plan ${statusText} ${statusEmoji}
303895
+ <img width="25" src="https://raw.githubusercontent.com/firestartr-pro/docs/refs/heads/main/logos/square-nobg.png"> ${operationTitle} ${statusText} ${statusEmoji}
303770
303896
  </h1>
303771
- <p><b>TFWorkspace: </b>${item.metadata.name}</p>
303897
+ <p><b>${kind}: </b>${name}</p>
303772
303898
 
303773
303899
  <details id=github>
303774
303900
  <summary>PLAN LOGS${partIndicator}</summary>
@@ -303784,7 +303910,9 @@ ${commentContent}
303784
303910
  owner: org,
303785
303911
  repo,
303786
303912
  pullNumber: prNumber,
303787
- baseKind: 'tfworkspace:plan',
303913
+ baseKind: [kind.toLowerCase(), name, operation, source]
303914
+ .filter(Boolean)
303915
+ .join(':'),
303788
303916
  bodies: commentBodies,
303789
303917
  });
303790
303918
  }
@@ -305636,91 +305764,6 @@ function waitForFile(filePath) {
305636
305764
  });
305637
305765
  }
305638
305766
 
305639
- ;// CONCATENATED MODULE: ../operator/src/tfworkspaceplans/index.ts
305640
-
305641
-
305642
-
305643
-
305644
-
305645
-
305646
-
305647
-
305648
- var FileStatus;
305649
- (function (FileStatus) {
305650
- FileStatus["DELETED"] = "removed";
305651
- FileStatus["ADDED"] = "added";
305652
- FileStatus["MODIFIED"] = "modified";
305653
- })(FileStatus || (FileStatus = {}));
305654
- function fDebug(message, level = 'info') {
305655
- console.log(JSON.stringify({ message, level }));
305656
- }
305657
- async function tfWorkspacePlan(opts) {
305658
- const { repo, owner, prNumber, namespace, ref } = opts;
305659
- const pull = `${owner}/${repo}#${prNumber}`;
305660
- let cr = null;
305661
- try {
305662
- fDebug(`Starting plan for ${pull}`, 'info');
305663
- await addPlanStatusCheck(pull, 'Terraform plan in progress...');
305664
- fDebug(`Getting PR ${prNumber} in ${repo}`, 'info');
305665
- const resp = await github.pulls.getPrFiles(prNumber, repo, owner);
305666
- const { data } = resp;
305667
- if (data.length !== 1) {
305668
- throw new Error(`One file expected in PR ${opts.prNumber} in ${opts.repo}, but found ${data.length}`);
305669
- }
305670
- const [file] = data;
305671
- if (!file) {
305672
- throw new Error(`No data found for PR ${opts.prNumber} in ${opts.repo}`);
305673
- }
305674
- const { filename, status } = file;
305675
- let content = '';
305676
- fDebug(`Getting content for ${filename} in ${repo}`);
305677
- if (status === FileStatus.DELETED) {
305678
- content = await github.repo.getContent(filename, repo, owner);
305679
- }
305680
- else if (status === FileStatus.ADDED || status === FileStatus.MODIFIED) {
305681
- content = await github.repo.getContent(filename, repo, owner, ref);
305682
- }
305683
- else {
305684
- throw new Error(`Unknown status ${status} for file ${filename} in PR ${opts.prNumber} in ${opts.repo}`);
305685
- }
305686
- cr = catalog_common.io.fromYaml(content);
305687
- if (cr.kind !== 'FirestartrTerraformWorkspace') {
305688
- throw new Error(`No FirestartrTerraformWorkspace found in PR ${opts.prNumber} in ${opts.repo}`);
305689
- }
305690
- fDebug('Resolving references');
305691
- const deps = await resolve(cr, getItemByItemPath, getSecret, namespace);
305692
- fDebug('Building context');
305693
- const ctx = await buildProvisionerContext(cr, deps);
305694
- fDebug('Context built');
305695
- const command = getCommandByStatus(status);
305696
- fDebug('Running terraform provisioner');
305697
- const tfOutput = await runTerraformProvisioner(ctx, command, undefined);
305698
- fDebug('Terraform provisioner finished');
305699
- fDebug('Publishing plan');
305700
- await publishPlan(cr, tfOutput, prNumber, repo, owner);
305701
- await addPlanStatusCheck(pull, tfOutput, 'completed');
305702
- }
305703
- catch (e) {
305704
- console.error(e);
305705
- const { output: message } = extractErrorDetails(e);
305706
- fDebug(`Error: ${message}`, 'error');
305707
- await addPlanStatusCheck(pull, message, 'completed', true);
305708
- fDebug('Publishing plan');
305709
- await publishPlan(cr, message, prNumber, repo, owner);
305710
- }
305711
- }
305712
- function getCommandByStatus(status) {
305713
- switch (status) {
305714
- case FileStatus.MODIFIED:
305715
- case FileStatus.ADDED:
305716
- return 'plan';
305717
- case FileStatus.DELETED:
305718
- return 'plan-destroy';
305719
- default:
305720
- throw new Error(`Unknown status: ${status}`);
305721
- }
305722
- }
305723
-
305724
305767
  ;// CONCATENATED MODULE: ../gh_provisioner/src/logger.ts
305725
305768
 
305726
305769
  /* harmony default export */ const gh_provisioner_src_logger = (catalog_common.logger);
@@ -305809,7 +305852,7 @@ const MODULES = {
305809
305852
  },
305810
305853
  FirestartrGithubRepository: {
305811
305854
  module: 'git::https://github.com/prefapp/tfm.git//modules/github-repo',
305812
- ref: 'github-repo-v0.2.0',
305855
+ ref: 'feat/1195-github-repo-problem-on-github-pages',
305813
305856
  },
305814
305857
  FirestartrGithubRepositoryFeature: {
305815
305858
  module: 'git::https://github.com/prefapp/tfm.git//modules/github-files-set',
@@ -306558,6 +306601,42 @@ class EntityGHRepo extends base_Entity {
306558
306601
  },
306559
306602
  });
306560
306603
  }
306604
+ /**
306605
+ * Validation logic for GitHub Pages branch settings:
306606
+ * - On create: If pages branch is set, it must equal the default branch.
306607
+ * - On update: If pages branch is set and is not the default, it must exist in remote.
306608
+ */
306609
+ async validatePagesBranch(tfOp, repoAlreadyExists) {
306610
+ const pages = this.cr.spec.pages;
306611
+ if (!pages || !pages.source || !pages.source.branch)
306612
+ return;
306613
+ const branch = pages.source.branch;
306614
+ const defaultBranch = this.cr.spec.repo.defaultBranch;
306615
+ const repo = this.cr.name;
306616
+ const org = this.cr.spec.org;
306617
+ gh_provisioner_src_logger.error(`[gh-provisioner] ${this.k8sId} validating pages branch '${branch}' against default branch '${defaultBranch}' for operation '${tfOp}'`);
306618
+ if (!repoAlreadyExists) {
306619
+ if (branch !== defaultBranch) {
306620
+ throw new Error(`Pages branch must equal default branch on creation. Provided: '${branch}', expected: '${defaultBranch}'`);
306621
+ }
306622
+ return;
306623
+ }
306624
+ else if (branch !== defaultBranch) {
306625
+ gh_provisioner_src_logger.error(`[gh-provisioner] ${this.k8sId} validating pages branch '${branch}' against default branch '${defaultBranch}' for operation '${tfOp}' - branch is different from default, checking existence in remote`);
306626
+ try {
306627
+ await this.runWithGithubProvider(async () => {
306628
+ await github.branches.getBranch(repo, branch, org);
306629
+ });
306630
+ }
306631
+ catch (err) {
306632
+ if (err && err.status === 404) {
306633
+ throw new Error(`Pages branch '${branch}' does not exist in the repository '${org}/${repo}'.`);
306634
+ }
306635
+ // Other errors propagate
306636
+ throw err;
306637
+ }
306638
+ }
306639
+ }
306561
306640
  async loadResources(tfOp) {
306562
306641
  let repoAlreadyExists = false;
306563
306642
  try {
@@ -306565,7 +306644,9 @@ class EntityGHRepo extends base_Entity {
306565
306644
  repoAlreadyExists = (await this.runWithGithubProvider(async () => {
306566
306645
  return await github.repo.repoExists(this.cr.spec.org, this.cr.name);
306567
306646
  }));
306647
+ await this.validatePagesBranch(tfOp, repoAlreadyExists);
306568
306648
  await this.provisionRepository();
306649
+ await this.provisionPages();
306569
306650
  await provisionDefaultBranch(this);
306570
306651
  await provisionCodeowners(this);
306571
306652
  await provisionVariables(this);
@@ -306673,10 +306754,21 @@ class EntityGHRepo extends base_Entity {
306673
306754
  ignoreVulnerabilityAlertsDuringRead: this.cr.spec.repo.ignoreVulnerabilityAlertsDuringRead,
306674
306755
  mergeCommitTitle: this.cr.spec.repo.mergeCommitTitle,
306675
306756
  squashMergeCommitMessage: this.cr.spec.repo.squashMergeCommitMessage,
306676
- pages: this.cr.spec.pages,
306677
306757
  },
306678
306758
  });
306679
306759
  }
306760
+ provisionPages() {
306761
+ if (this.cr.spec.pages) {
306762
+ this.patchData({
306763
+ path: '/config/pages',
306764
+ op: PatchOperations.add,
306765
+ value: {
306766
+ source: this.cr.spec.pages.source,
306767
+ cname: this.cr.spec.pages.cname,
306768
+ },
306769
+ });
306770
+ }
306771
+ }
306680
306772
  }
306681
306773
 
306682
306774
  ;// CONCATENATED MODULE: ../gh_provisioner/src/entities/ghfeature/helpers/managed_files.ts
@@ -307022,8 +307114,9 @@ class EntityGHMembership extends base_Entity {
307022
307114
  gh_provisioner_src_logger.debug(`[gh-provisioner] ${this.k8sId} loaded its data`);
307023
307115
  }
307024
307116
  catch (err) {
307025
- gh_provisioner_src_logger.error(`[gh-provisioner] ${this.k8sId} error loading resources: ${err}`);
307026
- throw `[gh-provisioner] ${this.k8sId} error loading resources: ${err}`;
307117
+ const message = err instanceof Error ? err.message : String(err);
307118
+ gh_provisioner_src_logger.error(`[gh-provisioner] ${this.k8sId} error loading resources: ${message}`);
307119
+ throw new Error(`[gh-provisioner] ${this.k8sId} error loading resources: ${message}`);
307027
307120
  }
307028
307121
  }
307029
307122
  async postProvision(tfOp) { }
@@ -307048,11 +307141,29 @@ class EntityGHMembership extends base_Entity {
307048
307141
  }
307049
307142
  }
307050
307143
  async provisionAllGroupMembershipRelation() {
307144
+ // Every org member must belong to the <org>-all team so they receive base
307145
+ // access. If that team is missing the provider likely points at the wrong
307146
+ // org, so we surface a clear error guiding the user toward the correct field.
307051
307147
  gh_provisioner_src_logger.debug('[gh-provisioner] provisioning all group membership relation');
307052
307148
  // we need to get the <org>-all group teamId
307053
- const teamInfo = await this.runWithGithubProvider(async () => {
307054
- return (await github.team.getTeamInfo(`${this.cr.spec.org}-all`, this.cr.spec.org));
307055
- });
307149
+ const allTeamName = `${this.cr.spec.org}-all`;
307150
+ let teamInfo;
307151
+ try {
307152
+ teamInfo = await this.runWithGithubProvider(async () => {
307153
+ return (await github.team.getTeamInfo(allTeamName, this.cr.spec.org));
307154
+ });
307155
+ }
307156
+ catch (err) {
307157
+ const status = err?.status || err?.response?.status;
307158
+ const message = err instanceof Error ? err.message : String(err);
307159
+ const isNotFound = status === 404 || message.includes('Not Found');
307160
+ if (isNotFound) {
307161
+ const claimRef = this.cr.metadata?.annotations?.['firestartr.dev/claim-ref'] ||
307162
+ 'unknown claim';
307163
+ throw new Error(`Could not find required GitHub team "${allTeamName}" in org "${this.cr.spec.org}" while loading membership for user "${this.cr.name}" (${claimRef}). Check UserClaim.providers.github.org / FirestartrGithubMembership.spec.org; it must point to the organization where the "<org>-all" team exists. Original error: ${message}`);
307164
+ }
307165
+ throw err;
307166
+ }
307056
307167
  gh_provisioner_src_logger.debug(`[gh-provisioner] got team info for org-all team: ${teamInfo.id}`);
307057
307168
  this.patchData({
307058
307169
  path: '/config/relationships/-',
@@ -307405,8 +307516,8 @@ async function runGhProvisioner(data, opts) {
307405
307516
  // ---------------------------------------
307406
307517
  // preparing the outputs
307407
307518
  // ---------------------------------------
307408
- if (tfOp === 'debug' || tfOp === 'nothing') {
307409
- gh_provisioner_src_logger.info(`[gh-provisioner] ${entity.k8sId} on debug or nothing: nothing is done`);
307519
+ if (tfOp === 'debug' || tfOp === 'nothing' || isPlanOperation(tfOp)) {
307520
+ gh_provisioner_src_logger.info(`[gh-provisioner] ${entity.k8sId} on ${tfOp}: post provision is skipped`);
307410
307521
  }
307411
307522
  else {
307412
307523
  gh_provisioner_src_logger.info(`[gh-provisioner] running post provision for ${entity.k8sId}`);
@@ -307422,13 +307533,14 @@ async function runGhProvisioner(data, opts) {
307422
307533
  return operationOutputs;
307423
307534
  }
307424
307535
  catch (err) {
307425
- gh_provisioner_src_logger.error(`[gh-provisioner] Error running runGhProvisioner: ${err}`);
307426
- if (!synthFinished)
307427
- entity.synthEnd(`${err}`);
307428
- if (entity && entity.inDebugMode) {
307536
+ const message = err instanceof Error ? err.message : String(err);
307537
+ gh_provisioner_src_logger.error(`[gh-provisioner] Error running runGhProvisioner: ${message}`);
307538
+ if (!synthFinished && entity)
307539
+ entity.synthEnd(message);
307540
+ if (entity && entity.inDebugMode && synthFinished) {
307429
307541
  await debugTerraformOutput(entity, err.toString());
307430
307542
  }
307431
- throw new Error(`[gh-provisioner] Error running runGhProvisioner: ${err}`);
307543
+ throw new Error(`[gh-provisioner] Error running runGhProvisioner: ${message}`);
307432
307544
  }
307433
307545
  finally {
307434
307546
  if (entity && entity.inDebugMode) {
@@ -307445,23 +307557,30 @@ function inferTFOperation(cr, opts) {
307445
307557
  ? true
307446
307558
  : false;
307447
307559
  gh_provisioner_src_logger.debug('[gh-provisioner] inferTFOperation options keys:', Object.keys(opts || {}));
307448
- const operation = isImport && needsReimport
307449
- ? 'import-with-reimport'
307450
- : isImport
307451
- ? 'import'
307452
- : opts.create
307453
- ? 'apply'
307454
- : opts.update
307455
- ? 'apply'
307456
- : opts.delete
307457
- ? 'destroy'
307458
- : opts.debug
307459
- ? 'debug'
307460
- : 'nothing';
307560
+ const operation = opts.plan
307561
+ ? 'plan'
307562
+ : opts.planDestroy
307563
+ ? 'plan-destroy'
307564
+ : isImport && needsReimport
307565
+ ? 'import-with-reimport'
307566
+ : isImport
307567
+ ? 'import'
307568
+ : opts.create
307569
+ ? 'apply'
307570
+ : opts.update
307571
+ ? 'apply'
307572
+ : opts.delete
307573
+ ? 'destroy'
307574
+ : opts.debug
307575
+ ? 'debug'
307576
+ : 'nothing';
307461
307577
  //opts.import && opts.skipPlan && isImport
307462
307578
  // ? 'IMPORT_SKIP_PLAN'
307463
307579
  return operation;
307464
307580
  }
307581
+ function isPlanOperation(tfOp) {
307582
+ return tfOp === 'plan' || tfOp === 'plan-destroy';
307583
+ }
307465
307584
  function sendWarnings() {
307466
307585
  if (process.env['AVOID_PROVIDER_SECRET_ENCRYPTION']) {
307467
307586
  console.warn(`
@@ -307488,6 +307607,446 @@ function sendWarnings() {
307488
307607
  runGhProvisioner,
307489
307608
  });
307490
307609
 
307610
+ ;// CONCATENATED MODULE: ../operator/src/pull-request-plan/index.ts
307611
+
307612
+
307613
+
307614
+
307615
+
307616
+
307617
+
307618
+
307619
+
307620
+
307621
+
307622
+ var FileStatus;
307623
+ (function (FileStatus) {
307624
+ FileStatus["DELETED"] = "removed";
307625
+ FileStatus["ADDED"] = "added";
307626
+ FileStatus["MODIFIED"] = "modified";
307627
+ FileStatus["RENAMED"] = "renamed";
307628
+ FileStatus["COPIED"] = "copied";
307629
+ FileStatus["CHANGED"] = "changed";
307630
+ FileStatus["UNCHANGED"] = "unchanged";
307631
+ })(FileStatus || (FileStatus = {}));
307632
+ const TERRAFORM_WORKSPACE_KIND = 'FirestartrTerraformWorkspace';
307633
+ const GITHUB_RESOURCE_KINDS = [
307634
+ 'FirestartrGithubGroup',
307635
+ 'FirestartrGithubMembership',
307636
+ 'FirestartrGithubRepository',
307637
+ 'FirestartrGithubRepositoryFeature',
307638
+ 'FirestartrGithubRepositorySecretsSection',
307639
+ 'FirestartrGithubOrgWebhook',
307640
+ ];
307641
+ const GITHUB_REPOSITORY_DEPENDENT_KINDS = [
307642
+ 'FirestartrGithubRepositoryFeature',
307643
+ 'FirestartrGithubRepositorySecretsSection',
307644
+ ];
307645
+ const SUPPORTED_RESOURCE_KINDS = [
307646
+ TERRAFORM_WORKSPACE_KIND,
307647
+ ...GITHUB_RESOURCE_KINDS,
307648
+ ];
307649
+ const SUMMARY_FAILURE_MAX_LENGTH = 160;
307650
+ const SUMMARY_DETAIL_MAX_ITEMS = 20;
307651
+ function fDebug(message, level = 'info') {
307652
+ console.log(JSON.stringify({ message, level }));
307653
+ }
307654
+ async function pullRequestPlan(opts) {
307655
+ const { repo, owner, prNumber } = opts;
307656
+ const pull = `${owner}/${repo}#${prNumber}`;
307657
+ try {
307658
+ fDebug(`Starting plan for ${pull}`, 'info');
307659
+ const headSha = await github.pulls.getPrLastCommitSHA(prNumber, repo, owner);
307660
+ if (!(await tryAcquirePlanLock(opts, headSha))) {
307661
+ fDebug(`Skipping duplicate plan for ${pull} at ${headSha}`, 'info');
307662
+ return;
307663
+ }
307664
+ await addPlanStatusCheck(pull, 'Plan in progress...');
307665
+ fDebug(`Getting PR ${prNumber} in ${repo}`, 'info');
307666
+ const resp = await github.pulls.getPrFiles(prNumber, repo, owner);
307667
+ const { data } = resp;
307668
+ if (data.length === 0) {
307669
+ throw new Error(`No data found for PR ${opts.prNumber} in ${opts.repo}`);
307670
+ }
307671
+ const planContext = {
307672
+ ...opts,
307673
+ baseResourceByItemPath: new Map(),
307674
+ fileContentByFilename: new Map(),
307675
+ prFileStatusByItemPath: new Map(),
307676
+ prResourceByItemPath: new Map(),
307677
+ };
307678
+ await indexPrResources(data, planContext);
307679
+ const results = [];
307680
+ for (const file of data) {
307681
+ results.push(await planFile(file, planContext));
307682
+ }
307683
+ const plannedResults = results.filter((result) => !result.skipped);
307684
+ if (plannedResults.length === 0) {
307685
+ throw new Error(`No supported Firestartr resources found in PR ${opts.prNumber} in ${opts.repo}`);
307686
+ }
307687
+ const failedResults = plannedResults.filter((result) => !result.success);
307688
+ const summary = buildSummary(results);
307689
+ await addPlanStatusCheck(pull, summary, 'completed', failedResults.length > 0);
307690
+ }
307691
+ catch (e) {
307692
+ console.error(e);
307693
+ const { output: message } = extractErrorDetails(e);
307694
+ fDebug(`Error: ${message}`, 'error');
307695
+ await addPlanStatusCheck(pull, message, 'completed', true);
307696
+ }
307697
+ }
307698
+ async function tryAcquirePlanLock(opts, headSha) {
307699
+ const { owner, repo, prNumber } = opts;
307700
+ const lockKey = `${owner}/${repo}#${prNumber}@${headSha}`;
307701
+ const lockHash = (0,external_crypto_.createHash)('sha256').update(lockKey).digest('hex');
307702
+ const lockRef = `refs/firestartr/plan-locks/${lockHash}`;
307703
+ try {
307704
+ return await github.repo.tryCreateRef(lockRef, headSha, repo, owner);
307705
+ }
307706
+ catch (e) {
307707
+ fDebug(`Could not create plan lock '${lockRef}' for '${lockKey}'; running without dedupe. Error: ${e?.message || e}`, 'warn');
307708
+ return true;
307709
+ }
307710
+ }
307711
+ async function planFile(file, opts) {
307712
+ const { repo, owner, prNumber } = opts;
307713
+ const { filename, status } = file;
307714
+ let cr;
307715
+ try {
307716
+ if (!pull_request_plan_isYamlFile(filename)) {
307717
+ return skippedResult(filename, 'Not a YAML file');
307718
+ }
307719
+ if (!isTrackedFileStatus(status)) {
307720
+ return skippedResult(filename, `Unsupported file status ${status}`);
307721
+ }
307722
+ fDebug(`Getting content for ${filename} in ${repo}`);
307723
+ const content = await getFileContent(filename, status, opts);
307724
+ cr = catalog_common.io.fromYaml(content);
307725
+ if (!cr || !cr.kind) {
307726
+ const message = `Missing Kubernetes kind in ${filename}`;
307727
+ await publishPlan(fallbackPlanResource(filename), message, prNumber, repo, owner, false, getCommandByStatus(status), filename);
307728
+ return {
307729
+ filename,
307730
+ kind: 'UnknownKind',
307731
+ name: filename,
307732
+ success: false,
307733
+ message,
307734
+ };
307735
+ }
307736
+ if (!SUPPORTED_RESOURCE_KINDS.includes(cr.kind)) {
307737
+ return skippedResult(filename, `Unsupported kind ${cr.kind}`);
307738
+ }
307739
+ const resourceName = getResourceName(cr);
307740
+ if (!resourceName) {
307741
+ const message = `Missing metadata.name or metadata.generateName in ${filename}`;
307742
+ await publishPlan(withFallbackResourceName(cr, filename), message, prNumber, repo, owner, false, getCommandByStatus(status), filename);
307743
+ return {
307744
+ filename,
307745
+ kind: cr.kind,
307746
+ name: filename,
307747
+ success: false,
307748
+ message,
307749
+ };
307750
+ }
307751
+ const skipReason = await getPlanSkipReason(cr, status, opts);
307752
+ if (skipReason) {
307753
+ await publishPlan(cr, skipReason, prNumber, repo, owner, true, `${getCommandByStatus(status)}-skipped`, filename);
307754
+ return {
307755
+ filename,
307756
+ kind: cr.kind,
307757
+ name: resourceName,
307758
+ skipped: true,
307759
+ success: true,
307760
+ message: skipReason,
307761
+ };
307762
+ }
307763
+ const output = isGithubResourceKind(cr.kind)
307764
+ ? await planGithubResource(cr, status, opts)
307765
+ : await planTerraformWorkspace(cr, status, opts);
307766
+ await publishPlan(cr, output, prNumber, repo, owner, true, getCommandByStatus(status), filename);
307767
+ return {
307768
+ filename,
307769
+ kind: cr.kind,
307770
+ name: resourceName,
307771
+ success: true,
307772
+ message: output,
307773
+ };
307774
+ }
307775
+ catch (e) {
307776
+ console.error(e);
307777
+ const { output: message } = extractErrorDetails(e);
307778
+ await publishPlan(cr && cr.kind ? cr : fallbackPlanResource(filename), message, prNumber, repo, owner, false, getSafeCommandByStatus(status), filename);
307779
+ return {
307780
+ filename,
307781
+ kind: cr?.kind,
307782
+ name: cr ? getResourceName(cr, filename) : undefined,
307783
+ success: false,
307784
+ message,
307785
+ };
307786
+ }
307787
+ }
307788
+ async function getFileContent(filename, status, opts) {
307789
+ const { repo, owner, prNumber, ref } = opts;
307790
+ const cachedContent = opts.fileContentByFilename.get(filename);
307791
+ if (cachedContent !== undefined)
307792
+ return cachedContent;
307793
+ let content;
307794
+ if (status === FileStatus.DELETED) {
307795
+ if (!opts.baseSha) {
307796
+ opts.baseSha = await github.pulls.getPrBaseSHA(prNumber, repo, owner);
307797
+ }
307798
+ content = await github.repo.getContent(filename, repo, owner, opts.baseSha);
307799
+ opts.fileContentByFilename.set(filename, content);
307800
+ return content;
307801
+ }
307802
+ if (status === FileStatus.ADDED ||
307803
+ status === FileStatus.MODIFIED ||
307804
+ status === FileStatus.RENAMED ||
307805
+ status === FileStatus.COPIED ||
307806
+ status === FileStatus.CHANGED) {
307807
+ content = await github.repo.getContent(filename, repo, owner, ref);
307808
+ opts.fileContentByFilename.set(filename, content);
307809
+ return content;
307810
+ }
307811
+ throw new Error(`Unknown status ${status} for file ${filename} in PR ${prNumber} in ${repo}`);
307812
+ }
307813
+ async function planTerraformWorkspace(cr, status, opts) {
307814
+ fDebug('Resolving references');
307815
+ const deps = await resolve(cr, getPrAwareItemByItemPath(opts), getSecret, opts.namespace);
307816
+ fDebug('Building context');
307817
+ const ctx = await buildProvisionerContext(cr, deps);
307818
+ fDebug('Context built');
307819
+ const command = getCommandByStatus(status);
307820
+ fDebug('Running terraform provisioner');
307821
+ const tfOutput = await runTerraformProvisioner(ctx, command, undefined);
307822
+ fDebug('Terraform provisioner finished');
307823
+ return tfOutput;
307824
+ }
307825
+ async function planGithubResource(cr, status, opts) {
307826
+ fDebug('Resolving references');
307827
+ const deps = await resolve(cr, getPrAwareItemByItemPath(opts), getSecret, opts.namespace);
307828
+ fDebug('Running GitHub provisioner plan');
307829
+ return await gh_provisioner.runGhProvisioner({
307830
+ mainCr: cr,
307831
+ deps,
307832
+ }, getGithubPlanOptionsByStatus(status));
307833
+ }
307834
+ async function indexPrResources(files, opts) {
307835
+ for (const file of files) {
307836
+ if (!pull_request_plan_isYamlFile(file.filename))
307837
+ continue;
307838
+ if (!isTrackedFileStatus(file.status))
307839
+ continue;
307840
+ try {
307841
+ const content = await getFileContent(file.filename, file.status, opts);
307842
+ const cr = catalog_common.io.fromYaml(content);
307843
+ const itemPath = getCrItemPath(cr, opts.namespace);
307844
+ if (itemPath) {
307845
+ opts.prFileStatusByItemPath.set(itemPath, file.status);
307846
+ if (file.status !== FileStatus.DELETED) {
307847
+ opts.prResourceByItemPath.set(itemPath, cr);
307848
+ }
307849
+ }
307850
+ }
307851
+ catch {
307852
+ // Invalid YAML or unsupported resources are reported by planFile.
307853
+ }
307854
+ }
307855
+ }
307856
+ async function getPlanSkipReason(cr, status, opts) {
307857
+ if (!isPrRefStatus(status))
307858
+ return undefined;
307859
+ if (!GITHUB_REPOSITORY_DEPENDENT_KINDS.includes(cr.kind))
307860
+ return undefined;
307861
+ const repositoryRef = cr.spec?.repositoryTarget?.ref;
307862
+ if (repositoryRef?.kind !== 'FirestartrGithubRepository' ||
307863
+ !repositoryRef.name) {
307864
+ return undefined;
307865
+ }
307866
+ const namespace = cr.metadata?.namespace || opts.namespace;
307867
+ const repositoryItemPath = `${namespace}/${definitions_getPluralFromKind(repositoryRef.kind)}/${repositoryRef.name}`;
307868
+ if (opts.prFileStatusByItemPath.get(repositoryItemPath) !== FileStatus.ADDED) {
307869
+ return undefined;
307870
+ }
307871
+ const baseRepository = await getBaseGithubResource(repositoryItemPath, opts);
307872
+ if (baseRepository)
307873
+ return undefined;
307874
+ return (`Skipped plan: ${cr.kind}/${getResourceName(cr)} targets ` +
307875
+ `${repositoryRef.kind}/${repositoryRef.name}, which is created in this PR. ` +
307876
+ 'The repository feature and secrets section can only be planned after the repository exists.');
307877
+ }
307878
+ function getPrAwareItemByItemPath(opts) {
307879
+ return async (itemPath, apiGroup, apiVersion) => {
307880
+ if (opts.prFileStatusByItemPath.get(itemPath) === FileStatus.DELETED) {
307881
+ return undefined;
307882
+ }
307883
+ const prResource = opts.prResourceByItemPath.get(itemPath);
307884
+ if (prResource)
307885
+ return prResource;
307886
+ const baseResource = await getBaseGithubResource(itemPath, opts);
307887
+ if (baseResource)
307888
+ return baseResource;
307889
+ return getItemByItemPath(itemPath, apiGroup, apiVersion);
307890
+ };
307891
+ }
307892
+ async function getBaseGithubResource(itemPath, opts) {
307893
+ if (opts.baseResourceByItemPath.has(itemPath)) {
307894
+ return opts.baseResourceByItemPath.get(itemPath) ?? undefined;
307895
+ }
307896
+ const resourceFile = getGithubResourceFilename(itemPath);
307897
+ if (!resourceFile)
307898
+ return undefined;
307899
+ if (!opts.baseSha) {
307900
+ opts.baseSha = await github.pulls.getPrBaseSHA(opts.prNumber, opts.repo, opts.owner);
307901
+ }
307902
+ try {
307903
+ const content = await github.repo.getContent(resourceFile, opts.repo, opts.owner, opts.baseSha);
307904
+ const cr = catalog_common.io.fromYaml(content);
307905
+ opts.baseResourceByItemPath.set(itemPath, cr);
307906
+ return cr;
307907
+ }
307908
+ catch (e) {
307909
+ if (e.status === 404 || e.message?.includes('Not Found')) {
307910
+ opts.baseResourceByItemPath.set(itemPath, null);
307911
+ return undefined;
307912
+ }
307913
+ throw e;
307914
+ }
307915
+ }
307916
+ function getGithubResourceFilename(itemPath) {
307917
+ const [, plural, name] = itemPath.match(/[^/]+\/([^/]+)\/([^/]+)$/) || [];
307918
+ const kind = getKindFromPlural(plural);
307919
+ if (!kind || !GITHUB_RESOURCE_KINDS.includes(kind))
307920
+ return undefined;
307921
+ return `${kind}.${name}.yaml`;
307922
+ }
307923
+ function getCrItemPath(cr, namespace) {
307924
+ const plural = cr?.kind ? definitions_getPluralFromKind(cr.kind) : undefined;
307925
+ const name = getResourceName(cr);
307926
+ if (!plural || !name)
307927
+ return undefined;
307928
+ return `${cr.metadata?.namespace || namespace}/${plural}/${name}`;
307929
+ }
307930
+ function isPrRefStatus(status) {
307931
+ return (status === FileStatus.ADDED ||
307932
+ status === FileStatus.MODIFIED ||
307933
+ status === FileStatus.RENAMED ||
307934
+ status === FileStatus.COPIED ||
307935
+ status === FileStatus.CHANGED);
307936
+ }
307937
+ function isTrackedFileStatus(status) {
307938
+ return isPrRefStatus(status) || status === FileStatus.DELETED;
307939
+ }
307940
+ function getGithubPlanOptionsByStatus(status) {
307941
+ switch (status) {
307942
+ case FileStatus.MODIFIED:
307943
+ case FileStatus.ADDED:
307944
+ case FileStatus.RENAMED:
307945
+ case FileStatus.COPIED:
307946
+ case FileStatus.CHANGED:
307947
+ return { plan: true };
307948
+ case FileStatus.DELETED:
307949
+ return { planDestroy: true };
307950
+ default:
307951
+ throw new Error(`Unknown status: ${status}`);
307952
+ }
307953
+ }
307954
+ function skippedResult(filename, message) {
307955
+ return {
307956
+ filename,
307957
+ skipped: true,
307958
+ success: true,
307959
+ message,
307960
+ };
307961
+ }
307962
+ function isGithubResourceKind(kind) {
307963
+ return GITHUB_RESOURCE_KINDS.includes(kind);
307964
+ }
307965
+ function pull_request_plan_isYamlFile(filename) {
307966
+ return filename.endsWith('.yaml') || filename.endsWith('.yml');
307967
+ }
307968
+ function getResourceName(cr, fallbackName) {
307969
+ return cr.metadata?.name || cr.metadata?.generateName || fallbackName;
307970
+ }
307971
+ function withFallbackResourceName(cr, filename) {
307972
+ return {
307973
+ ...cr,
307974
+ metadata: {
307975
+ ...cr.metadata,
307976
+ generateName: cr.metadata?.generateName || filename,
307977
+ },
307978
+ };
307979
+ }
307980
+ function fallbackPlanResource(filename) {
307981
+ return {
307982
+ kind: 'UnknownKind',
307983
+ metadata: {
307984
+ name: filename,
307985
+ },
307986
+ };
307987
+ }
307988
+ function buildSummary(results) {
307989
+ const planned = results.filter((result) => !result.skipped);
307990
+ const failed = planned.filter((result) => !result.success);
307991
+ const skipped = results.filter((result) => result.skipped);
307992
+ const summaryLines = [
307993
+ failed.length > 0 ? 'Plan failed' : 'Plan completed',
307994
+ `Planned resources: ${planned.length}`,
307995
+ `Failed resources: ${failed.length}`,
307996
+ `Skipped files: ${skipped.length}`,
307997
+ ];
307998
+ const detailLines = [];
307999
+ for (const result of planned) {
308000
+ const resource = [result.kind, result.name].filter(Boolean).join('/');
308001
+ const status = result.success ? 'success' : 'failed';
308002
+ const details = result.success
308003
+ ? ''
308004
+ : ` (${summarizeFailure(result.message)})`;
308005
+ detailLines.push(`- ${status}: ${resource || result.filename}${details}`);
308006
+ }
308007
+ for (const result of skipped) {
308008
+ const resource = [result.kind, result.name].filter(Boolean).join('/');
308009
+ detailLines.push(`- skipped: ${resource || result.filename} (${result.message})`);
308010
+ }
308011
+ const lines = [...summaryLines, ''];
308012
+ lines.push(...detailLines.slice(0, SUMMARY_DETAIL_MAX_ITEMS));
308013
+ const omittedDetails = detailLines.length - SUMMARY_DETAIL_MAX_ITEMS;
308014
+ if (omittedDetails > 0) {
308015
+ lines.push(`- omitted detail lines: ${omittedDetails}`);
308016
+ }
308017
+ lines.push('', ...summaryLines);
308018
+ return lines.join('\n');
308019
+ }
308020
+ function summarizeFailure(message) {
308021
+ const [firstLine = ''] = message.split('\n');
308022
+ if (firstLine.length <= SUMMARY_FAILURE_MAX_LENGTH) {
308023
+ return firstLine;
308024
+ }
308025
+ return `${firstLine.slice(0, SUMMARY_FAILURE_MAX_LENGTH - 3)}...`;
308026
+ }
308027
+ function getCommandByStatus(status) {
308028
+ switch (status) {
308029
+ case FileStatus.MODIFIED:
308030
+ case FileStatus.ADDED:
308031
+ case FileStatus.RENAMED:
308032
+ case FileStatus.COPIED:
308033
+ case FileStatus.CHANGED:
308034
+ return 'plan';
308035
+ case FileStatus.DELETED:
308036
+ return 'plan-destroy';
308037
+ default:
308038
+ throw new Error(`Unknown status: ${status}`);
308039
+ }
308040
+ }
308041
+ function getSafeCommandByStatus(status) {
308042
+ try {
308043
+ return getCommandByStatus(status);
308044
+ }
308045
+ catch {
308046
+ return 'plan';
308047
+ }
308048
+ }
308049
+
307491
308050
  ;// CONCATENATED MODULE: ../operator/src/ctl_collections.ts
307492
308051
 
307493
308052
 
@@ -308413,7 +308972,7 @@ const operatorSubcommands = {
308413
308972
  });
308414
308973
  }
308415
308974
  if (options['pull-request-plan']) {
308416
- await tfWorkspacePlan({
308975
+ await pullRequestPlan({
308417
308976
  prNumber: parseInt(options['prNumber']),
308418
308977
  repo: options['repo'],
308419
308978
  owner: options['owner'],
@@ -308770,7 +309329,7 @@ const crs_analyzerSubcommand = {
308770
309329
  };
308771
309330
 
308772
309331
  ;// CONCATENATED MODULE: ./package.json
308773
- const package_namespaceObject = JSON.parse('{"i8":"2.4.0-snapshot-2"}');
309332
+ const package_namespaceObject = JSON.parse('{"i8":"2.4.0-snapshot-4"}');
308774
309333
  ;// CONCATENATED MODULE: ../../package.json
308775
309334
  const package_namespaceObject_1 = {"i8":"2.3.0"};
308776
309335
  ;// CONCATENATED MODULE: ./src/subcommands/index.ts
@@ -1010,10 +1010,6 @@ export interface FirestartrGithubRepositorySpecRepo {
1010
1010
  * @schema FirestartrGithubRepositorySpecRepo#hasWiki
1011
1011
  */
1012
1012
  readonly hasWiki?: boolean;
1013
- /**
1014
- * @schema FirestartrGithubRepositorySpecRepo#pages
1015
- */
1016
- readonly pages?: any;
1017
1013
  /**
1018
1014
  * @schema FirestartrGithubRepositorySpecRepo#topics
1019
1015
  */
@@ -0,0 +1,6 @@
1
+ import { RenderClaims } from '../renderer/types';
2
+ /**
3
+ * Throws if any ComponentClaim's providers.github.pages.path is set to any value other than '/' or '/docs'.
4
+ * Should be called as a final render-time validation (never mutates input).
5
+ */
6
+ export declare function validateComponentPagesPath(renderClaims: RenderClaims): void;
@@ -1,8 +1,15 @@
1
1
  import { Entity } from '../base';
2
2
  export declare class EntityGHRepo extends Entity {
3
3
  constructor(artifact: any);
4
+ /**
5
+ * Validation logic for GitHub Pages branch settings:
6
+ * - On create: If pages branch is set, it must equal the default branch.
7
+ * - On update: If pages branch is set and is not the default, it must exist in remote.
8
+ */
9
+ private validatePagesBranch;
4
10
  loadResources(tfOp: string): Promise<void>;
5
11
  postProvision(tfOp: string): Promise<void>;
6
12
  loadAddressesToImport(): Promise<void>;
7
13
  provisionRepository(): Promise<void>;
14
+ provisionPages(): void;
8
15
  }
@@ -32,6 +32,7 @@ declare const _default: {
32
32
  getOIDCRepo: typeof import("./src/repository").getOIDCRepo;
33
33
  addStatusCheck: typeof import("./src/repository").addStatusCheck;
34
34
  addCommitStatus: typeof import("./src/repository").addCommitStatus;
35
+ tryCreateRef: typeof import("./src/repository").tryCreateRef;
35
36
  getRepoIssuesLabels: typeof import("./src/repository").getRepoIssuesLabels;
36
37
  };
37
38
  team: {
@@ -56,6 +57,7 @@ declare const _default: {
56
57
  commentInPR: typeof import("./src/pull_request").commentInPR;
57
58
  getPrLastCommitSHA: typeof import("./src/pull_request").getPrLastCommitSHA;
58
59
  getPrMergeCommitSHA: typeof import("./src/pull_request").getPrMergeCommitSHA;
60
+ getPrBaseSHA: typeof import("./src/pull_request").getPrBaseSHA;
59
61
  divideCommentIntoChunks: typeof import("./src/pull_request").divideCommentIntoChunks;
60
62
  getPrFiles: typeof import("./src/pull_request").getPrFiles;
61
63
  filterPrBy: (filter: {
@@ -1,6 +1,7 @@
1
1
  export declare function commentInPR(comment: string, pr_number: number, repo: string, owner?: string, stickyKind?: string): Promise<void>;
2
2
  export declare function getPrLastCommitSHA(pull_number: number, repo: string, owner?: string): Promise<string>;
3
3
  export declare function getPrMergeCommitSHA(pull_number: number, repo: string, owner?: string): Promise<string>;
4
+ export declare function getPrBaseSHA(pull_number: number, repo: string, owner?: string): Promise<string>;
4
5
  export declare function divideCommentIntoChunks(comment: string, sizeReduction?: number): string[];
5
6
  export declare function getPrFiles(pr_number: number, repo: string, owner?: string): Promise<any>;
6
7
  declare function filterPrBy(filter: {
@@ -16,6 +17,7 @@ declare const _default: {
16
17
  commentInPR: typeof commentInPR;
17
18
  getPrLastCommitSHA: typeof getPrLastCommitSHA;
18
19
  getPrMergeCommitSHA: typeof getPrMergeCommitSHA;
20
+ getPrBaseSHA: typeof getPrBaseSHA;
19
21
  divideCommentIntoChunks: typeof divideCommentIntoChunks;
20
22
  getPrFiles: typeof getPrFiles;
21
23
  filterPrBy: typeof filterPrBy;
@@ -1322,6 +1322,7 @@ export declare function uploadFile(destinationPath: string, filePath: string, re
1322
1322
  export declare function deleteFile(path: string, repo: string, owner?: string, branch?: string, message?: string): Promise<void>;
1323
1323
  export declare function addStatusCheck(output: any, is_failure: boolean, head_sha: string, name: string, status: string, repo: string, owner?: string): Promise<void>;
1324
1324
  export declare function addCommitStatus(state: commitStatusState, sha: string, repo: string, owner?: string, target_url?: string, description?: string, context?: string): Promise<void>;
1325
+ export declare function tryCreateRef(ref: string, sha: string, repo: string, owner?: string): Promise<boolean>;
1325
1326
  export declare function getRepoIssuesLabels(owner: string, repo: string): Promise<{
1326
1327
  name: string;
1327
1328
  description: string;
@@ -1343,6 +1344,7 @@ declare const _default: {
1343
1344
  getOIDCRepo: typeof getOIDCRepo;
1344
1345
  addStatusCheck: typeof addStatusCheck;
1345
1346
  addCommitStatus: typeof addCommitStatus;
1347
+ tryCreateRef: typeof tryCreateRef;
1346
1348
  getRepoIssuesLabels: typeof getRepoIssuesLabels;
1347
1349
  };
1348
1350
  export default _default;
@@ -1,5 +1,5 @@
1
1
  export { execTfCommand } from './src/execTfCmd';
2
- export { tfWorkspacePlan } from './src/tfworkspaceplans';
2
+ export { pullRequestPlan } from './src/pull-request-plan';
3
3
  export * as cmd from './src/cmd';
4
4
  export declare function isImportMode(): boolean;
5
5
  export declare function isImportModeSkipPlan(): boolean;
@@ -5,5 +5,5 @@ interface PlanOptions {
5
5
  namespace: string;
6
6
  ref: string;
7
7
  }
8
- export declare function tfWorkspacePlan(opts: PlanOptions): Promise<void>;
8
+ export declare function pullRequestPlan(opts: PlanOptions): Promise<void>;
9
9
  export {};
@@ -9,4 +9,4 @@ export declare function extractPrInfo(item: any, annotation?: 'firestartr.dev/la
9
9
  };
10
10
  export declare function tryPublishError(item: any, reason: string, message: string): Promise<void>;
11
11
  export declare function publishError(item: any, reason: string, message: string): Promise<void>;
12
- export declare function publishPlan(item: any, planOutput: string, prNumber: number, repo: string, org: string, isSuccess?: boolean): Promise<void>;
12
+ export declare function publishPlan(item: any, planOutput: string, prNumber: number, repo: string, org: string, isSuccess?: boolean, operation?: string, source?: string): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firestartr/cli",
3
- "version": "2.4.0-snapshot-2",
3
+ "version": "2.4.0-snapshot-4",
4
4
  "private": false,
5
5
  "description": "Commandline tool",
6
6
  "main": "build/main.js",