@firestartr/cli 2.4.0-snapshot-1 → 2.4.0-snapshot-3

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'];
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() {
@@ -298464,6 +298583,20 @@ class RepoGithubDecanter extends GithubDecanter {
298464
298583
  }
298465
298584
  importer_src_logger.info(`No CODEOWNERS file found for ${this.data.repoDetails.name}, skipping.`);
298466
298585
  }
298586
+ async __gatherPagesConfiguration() {
298587
+ try {
298588
+ const pagesConfig = await github.repo.getPages(this.org, this.data.repoDetails.name);
298589
+ this.data['pages'] = pagesConfig;
298590
+ }
298591
+ catch (e) {
298592
+ const status = e?.status ?? e?.response?.status;
298593
+ if (status === 404) {
298594
+ importer_src_logger.info(`No GitHub Pages configuration found for ${this.data.repoDetails.name}, skipping.`);
298595
+ return;
298596
+ }
298597
+ throw e;
298598
+ }
298599
+ }
298467
298600
  async __gatherRepoLabels() {
298468
298601
  try {
298469
298602
  const labels = await github.repo.getRepoIssuesLabels(this.org, this.data.repoDetails.name);
@@ -300614,8 +300747,9 @@ async function resolveDep(namespace, resolve, deps, references, getItemByItemPat
300614
300747
  for (const nestedRef of resolver_walk(ref)) {
300615
300748
  references.push(nestedRef);
300616
300749
  }
300617
- const secret = resolve.apiGroup === catalog_common.types.controller.FirestartrApiGroup ||
300618
- resolve.apiGroup === catalog_common.types.controller.ExternalSecretsApiGroup
300750
+ const secret = resolve.needsSecret !== false &&
300751
+ (resolve.apiGroup === catalog_common.types.controller.FirestartrApiGroup ||
300752
+ resolve.apiGroup === catalog_common.types.controller.ExternalSecretsApiGroup)
300619
300753
  ? await resolveSecretRef(namespace, ref, getSecret)
300620
300754
  : undefined;
300621
300755
  deps[kr] = {
@@ -303757,18 +303891,24 @@ ${message}
303757
303891
  `;
303758
303892
  await github.pulls.commentInPR(comment, prNumber, repo, org, 'logs');
303759
303893
  }
303760
- async function publishPlan(item, planOutput, prNumber, repo, org, isSuccess = true) {
303894
+ async function publishPlan(item, planOutput, prNumber, repo, org, isSuccess = true, operation = 'plan', source = '') {
303761
303895
  try {
303896
+ const kind = item.kind || 'UnknownKind';
303897
+ const name = item.metadata?.name || item.metadata?.generateName || 'unknown';
303762
303898
  const dividedOutput = github.pulls.divideCommentIntoChunks(planOutput, 250);
303763
303899
  const statusEmoji = isSuccess ? '✅' : '❌';
303764
303900
  const statusText = isSuccess ? 'Succeeded' : 'Failed';
303901
+ const operationTitle = operation
303902
+ .split('-')
303903
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
303904
+ .join('-');
303765
303905
  const commentBodies = dividedOutput.map((commentContent, index) => {
303766
303906
  const isMultiPart = dividedOutput.length > 1;
303767
303907
  const partIndicator = isMultiPart ? ` (Part ${index + 1})` : '';
303768
303908
  return `<h1>
303769
- <img width="25" src="https://raw.githubusercontent.com/firestartr-pro/docs/refs/heads/main/logos/square-nobg.png"> Plan ${statusText} ${statusEmoji}
303909
+ <img width="25" src="https://raw.githubusercontent.com/firestartr-pro/docs/refs/heads/main/logos/square-nobg.png"> ${operationTitle} ${statusText} ${statusEmoji}
303770
303910
  </h1>
303771
- <p><b>TFWorkspace: </b>${item.metadata.name}</p>
303911
+ <p><b>${kind}: </b>${name}</p>
303772
303912
 
303773
303913
  <details id=github>
303774
303914
  <summary>PLAN LOGS${partIndicator}</summary>
@@ -303784,7 +303924,9 @@ ${commentContent}
303784
303924
  owner: org,
303785
303925
  repo,
303786
303926
  pullNumber: prNumber,
303787
- baseKind: 'tfworkspace:plan',
303927
+ baseKind: [kind.toLowerCase(), name, operation, source]
303928
+ .filter(Boolean)
303929
+ .join(':'),
303788
303930
  bodies: commentBodies,
303789
303931
  });
303790
303932
  }
@@ -305636,91 +305778,6 @@ function waitForFile(filePath) {
305636
305778
  });
305637
305779
  }
305638
305780
 
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
305781
  ;// CONCATENATED MODULE: ../gh_provisioner/src/logger.ts
305725
305782
 
305726
305783
  /* harmony default export */ const gh_provisioner_src_logger = (catalog_common.logger);
@@ -305809,7 +305866,7 @@ const MODULES = {
305809
305866
  },
305810
305867
  FirestartrGithubRepository: {
305811
305868
  module: 'git::https://github.com/prefapp/tfm.git//modules/github-repo',
305812
- ref: 'github-repo-v0.2.0',
305869
+ ref: 'feat/1195-github-repo-problem-on-github-pages',
305813
305870
  },
305814
305871
  FirestartrGithubRepositoryFeature: {
305815
305872
  module: 'git::https://github.com/prefapp/tfm.git//modules/github-files-set',
@@ -306558,6 +306615,42 @@ class EntityGHRepo extends base_Entity {
306558
306615
  },
306559
306616
  });
306560
306617
  }
306618
+ /**
306619
+ * Validation logic for GitHub Pages branch settings:
306620
+ * - On create: If pages branch is set, it must equal the default branch.
306621
+ * - On update: If pages branch is set and is not the default, it must exist in remote.
306622
+ */
306623
+ async validatePagesBranch(tfOp, repoAlreadyExists) {
306624
+ const pages = this.cr.spec.pages;
306625
+ if (!pages || !pages.source || !pages.source.branch)
306626
+ return;
306627
+ const branch = pages.source.branch;
306628
+ const defaultBranch = this.cr.spec.repo.defaultBranch;
306629
+ const repo = this.cr.name;
306630
+ const org = this.cr.spec.org;
306631
+ gh_provisioner_src_logger.error(`[gh-provisioner] ${this.k8sId} validating pages branch '${branch}' against default branch '${defaultBranch}' for operation '${tfOp}'`);
306632
+ if (!repoAlreadyExists) {
306633
+ if (branch !== defaultBranch) {
306634
+ throw new Error(`Pages branch must equal default branch on creation. Provided: '${branch}', expected: '${defaultBranch}'`);
306635
+ }
306636
+ return;
306637
+ }
306638
+ else if (branch !== defaultBranch) {
306639
+ 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`);
306640
+ try {
306641
+ await this.runWithGithubProvider(async () => {
306642
+ await github.branches.getBranch(repo, branch, org);
306643
+ });
306644
+ }
306645
+ catch (err) {
306646
+ if (err && err.status === 404) {
306647
+ throw new Error(`Pages branch '${branch}' does not exist in the repository '${org}/${repo}'.`);
306648
+ }
306649
+ // Other errors propagate
306650
+ throw err;
306651
+ }
306652
+ }
306653
+ }
306561
306654
  async loadResources(tfOp) {
306562
306655
  let repoAlreadyExists = false;
306563
306656
  try {
@@ -306565,7 +306658,9 @@ class EntityGHRepo extends base_Entity {
306565
306658
  repoAlreadyExists = (await this.runWithGithubProvider(async () => {
306566
306659
  return await github.repo.repoExists(this.cr.spec.org, this.cr.name);
306567
306660
  }));
306661
+ await this.validatePagesBranch(tfOp, repoAlreadyExists);
306568
306662
  await this.provisionRepository();
306663
+ await this.provisionPages();
306569
306664
  await provisionDefaultBranch(this);
306570
306665
  await provisionCodeowners(this);
306571
306666
  await provisionVariables(this);
@@ -306673,10 +306768,21 @@ class EntityGHRepo extends base_Entity {
306673
306768
  ignoreVulnerabilityAlertsDuringRead: this.cr.spec.repo.ignoreVulnerabilityAlertsDuringRead,
306674
306769
  mergeCommitTitle: this.cr.spec.repo.mergeCommitTitle,
306675
306770
  squashMergeCommitMessage: this.cr.spec.repo.squashMergeCommitMessage,
306676
- pages: this.cr.spec.pages,
306677
306771
  },
306678
306772
  });
306679
306773
  }
306774
+ provisionPages() {
306775
+ if (this.cr.spec.pages) {
306776
+ this.patchData({
306777
+ path: '/config/pages',
306778
+ op: PatchOperations.add,
306779
+ value: {
306780
+ source: this.cr.spec.pages.source,
306781
+ cname: this.cr.spec.pages.cname,
306782
+ },
306783
+ });
306784
+ }
306785
+ }
306680
306786
  }
306681
306787
 
306682
306788
  ;// CONCATENATED MODULE: ../gh_provisioner/src/entities/ghfeature/helpers/managed_files.ts
@@ -307022,8 +307128,9 @@ class EntityGHMembership extends base_Entity {
307022
307128
  gh_provisioner_src_logger.debug(`[gh-provisioner] ${this.k8sId} loaded its data`);
307023
307129
  }
307024
307130
  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}`;
307131
+ const message = err instanceof Error ? err.message : String(err);
307132
+ gh_provisioner_src_logger.error(`[gh-provisioner] ${this.k8sId} error loading resources: ${message}`);
307133
+ throw new Error(`[gh-provisioner] ${this.k8sId} error loading resources: ${message}`);
307027
307134
  }
307028
307135
  }
307029
307136
  async postProvision(tfOp) { }
@@ -307048,11 +307155,29 @@ class EntityGHMembership extends base_Entity {
307048
307155
  }
307049
307156
  }
307050
307157
  async provisionAllGroupMembershipRelation() {
307158
+ // Every org member must belong to the <org>-all team so they receive base
307159
+ // access. If that team is missing the provider likely points at the wrong
307160
+ // org, so we surface a clear error guiding the user toward the correct field.
307051
307161
  gh_provisioner_src_logger.debug('[gh-provisioner] provisioning all group membership relation');
307052
307162
  // 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
- });
307163
+ const allTeamName = `${this.cr.spec.org}-all`;
307164
+ let teamInfo;
307165
+ try {
307166
+ teamInfo = await this.runWithGithubProvider(async () => {
307167
+ return (await github.team.getTeamInfo(allTeamName, this.cr.spec.org));
307168
+ });
307169
+ }
307170
+ catch (err) {
307171
+ const status = err?.status || err?.response?.status;
307172
+ const message = err instanceof Error ? err.message : String(err);
307173
+ const isNotFound = status === 404 || message.includes('Not Found');
307174
+ if (isNotFound) {
307175
+ const claimRef = this.cr.metadata?.annotations?.['firestartr.dev/claim-ref'] ||
307176
+ 'unknown claim';
307177
+ 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}`);
307178
+ }
307179
+ throw err;
307180
+ }
307056
307181
  gh_provisioner_src_logger.debug(`[gh-provisioner] got team info for org-all team: ${teamInfo.id}`);
307057
307182
  this.patchData({
307058
307183
  path: '/config/relationships/-',
@@ -307405,8 +307530,8 @@ async function runGhProvisioner(data, opts) {
307405
307530
  // ---------------------------------------
307406
307531
  // preparing the outputs
307407
307532
  // ---------------------------------------
307408
- if (tfOp === 'debug' || tfOp === 'nothing') {
307409
- gh_provisioner_src_logger.info(`[gh-provisioner] ${entity.k8sId} on debug or nothing: nothing is done`);
307533
+ if (tfOp === 'debug' || tfOp === 'nothing' || isPlanOperation(tfOp)) {
307534
+ gh_provisioner_src_logger.info(`[gh-provisioner] ${entity.k8sId} on ${tfOp}: post provision is skipped`);
307410
307535
  }
307411
307536
  else {
307412
307537
  gh_provisioner_src_logger.info(`[gh-provisioner] running post provision for ${entity.k8sId}`);
@@ -307422,13 +307547,14 @@ async function runGhProvisioner(data, opts) {
307422
307547
  return operationOutputs;
307423
307548
  }
307424
307549
  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) {
307550
+ const message = err instanceof Error ? err.message : String(err);
307551
+ gh_provisioner_src_logger.error(`[gh-provisioner] Error running runGhProvisioner: ${message}`);
307552
+ if (!synthFinished && entity)
307553
+ entity.synthEnd(message);
307554
+ if (entity && entity.inDebugMode && synthFinished) {
307429
307555
  await debugTerraformOutput(entity, err.toString());
307430
307556
  }
307431
- throw new Error(`[gh-provisioner] Error running runGhProvisioner: ${err}`);
307557
+ throw new Error(`[gh-provisioner] Error running runGhProvisioner: ${message}`);
307432
307558
  }
307433
307559
  finally {
307434
307560
  if (entity && entity.inDebugMode) {
@@ -307445,23 +307571,30 @@ function inferTFOperation(cr, opts) {
307445
307571
  ? true
307446
307572
  : false;
307447
307573
  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';
307574
+ const operation = opts.plan
307575
+ ? 'plan'
307576
+ : opts.planDestroy
307577
+ ? 'plan-destroy'
307578
+ : isImport && needsReimport
307579
+ ? 'import-with-reimport'
307580
+ : isImport
307581
+ ? 'import'
307582
+ : opts.create
307583
+ ? 'apply'
307584
+ : opts.update
307585
+ ? 'apply'
307586
+ : opts.delete
307587
+ ? 'destroy'
307588
+ : opts.debug
307589
+ ? 'debug'
307590
+ : 'nothing';
307461
307591
  //opts.import && opts.skipPlan && isImport
307462
307592
  // ? 'IMPORT_SKIP_PLAN'
307463
307593
  return operation;
307464
307594
  }
307595
+ function isPlanOperation(tfOp) {
307596
+ return tfOp === 'plan' || tfOp === 'plan-destroy';
307597
+ }
307465
307598
  function sendWarnings() {
307466
307599
  if (process.env['AVOID_PROVIDER_SECRET_ENCRYPTION']) {
307467
307600
  console.warn(`
@@ -307488,6 +307621,446 @@ function sendWarnings() {
307488
307621
  runGhProvisioner,
307489
307622
  });
307490
307623
 
307624
+ ;// CONCATENATED MODULE: ../operator/src/pull-request-plan/index.ts
307625
+
307626
+
307627
+
307628
+
307629
+
307630
+
307631
+
307632
+
307633
+
307634
+
307635
+
307636
+ var FileStatus;
307637
+ (function (FileStatus) {
307638
+ FileStatus["DELETED"] = "removed";
307639
+ FileStatus["ADDED"] = "added";
307640
+ FileStatus["MODIFIED"] = "modified";
307641
+ FileStatus["RENAMED"] = "renamed";
307642
+ FileStatus["COPIED"] = "copied";
307643
+ FileStatus["CHANGED"] = "changed";
307644
+ FileStatus["UNCHANGED"] = "unchanged";
307645
+ })(FileStatus || (FileStatus = {}));
307646
+ const TERRAFORM_WORKSPACE_KIND = 'FirestartrTerraformWorkspace';
307647
+ const GITHUB_RESOURCE_KINDS = [
307648
+ 'FirestartrGithubGroup',
307649
+ 'FirestartrGithubMembership',
307650
+ 'FirestartrGithubRepository',
307651
+ 'FirestartrGithubRepositoryFeature',
307652
+ 'FirestartrGithubRepositorySecretsSection',
307653
+ 'FirestartrGithubOrgWebhook',
307654
+ ];
307655
+ const GITHUB_REPOSITORY_DEPENDENT_KINDS = [
307656
+ 'FirestartrGithubRepositoryFeature',
307657
+ 'FirestartrGithubRepositorySecretsSection',
307658
+ ];
307659
+ const SUPPORTED_RESOURCE_KINDS = [
307660
+ TERRAFORM_WORKSPACE_KIND,
307661
+ ...GITHUB_RESOURCE_KINDS,
307662
+ ];
307663
+ const SUMMARY_FAILURE_MAX_LENGTH = 160;
307664
+ const SUMMARY_DETAIL_MAX_ITEMS = 20;
307665
+ function fDebug(message, level = 'info') {
307666
+ console.log(JSON.stringify({ message, level }));
307667
+ }
307668
+ async function pullRequestPlan(opts) {
307669
+ const { repo, owner, prNumber } = opts;
307670
+ const pull = `${owner}/${repo}#${prNumber}`;
307671
+ try {
307672
+ fDebug(`Starting plan for ${pull}`, 'info');
307673
+ const headSha = await github.pulls.getPrLastCommitSHA(prNumber, repo, owner);
307674
+ if (!(await tryAcquirePlanLock(opts, headSha))) {
307675
+ fDebug(`Skipping duplicate plan for ${pull} at ${headSha}`, 'info');
307676
+ return;
307677
+ }
307678
+ await addPlanStatusCheck(pull, 'Plan in progress...');
307679
+ fDebug(`Getting PR ${prNumber} in ${repo}`, 'info');
307680
+ const resp = await github.pulls.getPrFiles(prNumber, repo, owner);
307681
+ const { data } = resp;
307682
+ if (data.length === 0) {
307683
+ throw new Error(`No data found for PR ${opts.prNumber} in ${opts.repo}`);
307684
+ }
307685
+ const planContext = {
307686
+ ...opts,
307687
+ baseResourceByItemPath: new Map(),
307688
+ fileContentByFilename: new Map(),
307689
+ prFileStatusByItemPath: new Map(),
307690
+ prResourceByItemPath: new Map(),
307691
+ };
307692
+ await indexPrResources(data, planContext);
307693
+ const results = [];
307694
+ for (const file of data) {
307695
+ results.push(await planFile(file, planContext));
307696
+ }
307697
+ const plannedResults = results.filter((result) => !result.skipped);
307698
+ if (plannedResults.length === 0) {
307699
+ throw new Error(`No supported Firestartr resources found in PR ${opts.prNumber} in ${opts.repo}`);
307700
+ }
307701
+ const failedResults = plannedResults.filter((result) => !result.success);
307702
+ const summary = buildSummary(results);
307703
+ await addPlanStatusCheck(pull, summary, 'completed', failedResults.length > 0);
307704
+ }
307705
+ catch (e) {
307706
+ console.error(e);
307707
+ const { output: message } = extractErrorDetails(e);
307708
+ fDebug(`Error: ${message}`, 'error');
307709
+ await addPlanStatusCheck(pull, message, 'completed', true);
307710
+ }
307711
+ }
307712
+ async function tryAcquirePlanLock(opts, headSha) {
307713
+ const { owner, repo, prNumber } = opts;
307714
+ const lockKey = `${owner}/${repo}#${prNumber}@${headSha}`;
307715
+ const lockHash = (0,external_crypto_.createHash)('sha256').update(lockKey).digest('hex');
307716
+ const lockRef = `refs/firestartr/plan-locks/${lockHash}`;
307717
+ try {
307718
+ return await github.repo.tryCreateRef(lockRef, headSha, repo, owner);
307719
+ }
307720
+ catch (e) {
307721
+ fDebug(`Could not create plan lock '${lockRef}' for '${lockKey}'; running without dedupe. Error: ${e?.message || e}`, 'warn');
307722
+ return true;
307723
+ }
307724
+ }
307725
+ async function planFile(file, opts) {
307726
+ const { repo, owner, prNumber } = opts;
307727
+ const { filename, status } = file;
307728
+ let cr;
307729
+ try {
307730
+ if (!pull_request_plan_isYamlFile(filename)) {
307731
+ return skippedResult(filename, 'Not a YAML file');
307732
+ }
307733
+ if (!isTrackedFileStatus(status)) {
307734
+ return skippedResult(filename, `Unsupported file status ${status}`);
307735
+ }
307736
+ fDebug(`Getting content for ${filename} in ${repo}`);
307737
+ const content = await getFileContent(filename, status, opts);
307738
+ cr = catalog_common.io.fromYaml(content);
307739
+ if (!cr || !cr.kind) {
307740
+ const message = `Missing Kubernetes kind in ${filename}`;
307741
+ await publishPlan(fallbackPlanResource(filename), message, prNumber, repo, owner, false, getCommandByStatus(status), filename);
307742
+ return {
307743
+ filename,
307744
+ kind: 'UnknownKind',
307745
+ name: filename,
307746
+ success: false,
307747
+ message,
307748
+ };
307749
+ }
307750
+ if (!SUPPORTED_RESOURCE_KINDS.includes(cr.kind)) {
307751
+ return skippedResult(filename, `Unsupported kind ${cr.kind}`);
307752
+ }
307753
+ const resourceName = getResourceName(cr);
307754
+ if (!resourceName) {
307755
+ const message = `Missing metadata.name or metadata.generateName in ${filename}`;
307756
+ await publishPlan(withFallbackResourceName(cr, filename), message, prNumber, repo, owner, false, getCommandByStatus(status), filename);
307757
+ return {
307758
+ filename,
307759
+ kind: cr.kind,
307760
+ name: filename,
307761
+ success: false,
307762
+ message,
307763
+ };
307764
+ }
307765
+ const skipReason = await getPlanSkipReason(cr, status, opts);
307766
+ if (skipReason) {
307767
+ await publishPlan(cr, skipReason, prNumber, repo, owner, true, `${getCommandByStatus(status)}-skipped`, filename);
307768
+ return {
307769
+ filename,
307770
+ kind: cr.kind,
307771
+ name: resourceName,
307772
+ skipped: true,
307773
+ success: true,
307774
+ message: skipReason,
307775
+ };
307776
+ }
307777
+ const output = isGithubResourceKind(cr.kind)
307778
+ ? await planGithubResource(cr, status, opts)
307779
+ : await planTerraformWorkspace(cr, status, opts);
307780
+ await publishPlan(cr, output, prNumber, repo, owner, true, getCommandByStatus(status), filename);
307781
+ return {
307782
+ filename,
307783
+ kind: cr.kind,
307784
+ name: resourceName,
307785
+ success: true,
307786
+ message: output,
307787
+ };
307788
+ }
307789
+ catch (e) {
307790
+ console.error(e);
307791
+ const { output: message } = extractErrorDetails(e);
307792
+ await publishPlan(cr && cr.kind ? cr : fallbackPlanResource(filename), message, prNumber, repo, owner, false, getSafeCommandByStatus(status), filename);
307793
+ return {
307794
+ filename,
307795
+ kind: cr?.kind,
307796
+ name: cr ? getResourceName(cr, filename) : undefined,
307797
+ success: false,
307798
+ message,
307799
+ };
307800
+ }
307801
+ }
307802
+ async function getFileContent(filename, status, opts) {
307803
+ const { repo, owner, prNumber, ref } = opts;
307804
+ const cachedContent = opts.fileContentByFilename.get(filename);
307805
+ if (cachedContent !== undefined)
307806
+ return cachedContent;
307807
+ let content;
307808
+ if (status === FileStatus.DELETED) {
307809
+ if (!opts.baseSha) {
307810
+ opts.baseSha = await github.pulls.getPrBaseSHA(prNumber, repo, owner);
307811
+ }
307812
+ content = await github.repo.getContent(filename, repo, owner, opts.baseSha);
307813
+ opts.fileContentByFilename.set(filename, content);
307814
+ return content;
307815
+ }
307816
+ if (status === FileStatus.ADDED ||
307817
+ status === FileStatus.MODIFIED ||
307818
+ status === FileStatus.RENAMED ||
307819
+ status === FileStatus.COPIED ||
307820
+ status === FileStatus.CHANGED) {
307821
+ content = await github.repo.getContent(filename, repo, owner, ref);
307822
+ opts.fileContentByFilename.set(filename, content);
307823
+ return content;
307824
+ }
307825
+ throw new Error(`Unknown status ${status} for file ${filename} in PR ${prNumber} in ${repo}`);
307826
+ }
307827
+ async function planTerraformWorkspace(cr, status, opts) {
307828
+ fDebug('Resolving references');
307829
+ const deps = await resolve(cr, getPrAwareItemByItemPath(opts), getSecret, opts.namespace);
307830
+ fDebug('Building context');
307831
+ const ctx = await buildProvisionerContext(cr, deps);
307832
+ fDebug('Context built');
307833
+ const command = getCommandByStatus(status);
307834
+ fDebug('Running terraform provisioner');
307835
+ const tfOutput = await runTerraformProvisioner(ctx, command, undefined);
307836
+ fDebug('Terraform provisioner finished');
307837
+ return tfOutput;
307838
+ }
307839
+ async function planGithubResource(cr, status, opts) {
307840
+ fDebug('Resolving references');
307841
+ const deps = await resolve(cr, getPrAwareItemByItemPath(opts), getSecret, opts.namespace);
307842
+ fDebug('Running GitHub provisioner plan');
307843
+ return await gh_provisioner.runGhProvisioner({
307844
+ mainCr: cr,
307845
+ deps,
307846
+ }, getGithubPlanOptionsByStatus(status));
307847
+ }
307848
+ async function indexPrResources(files, opts) {
307849
+ for (const file of files) {
307850
+ if (!pull_request_plan_isYamlFile(file.filename))
307851
+ continue;
307852
+ if (!isTrackedFileStatus(file.status))
307853
+ continue;
307854
+ try {
307855
+ const content = await getFileContent(file.filename, file.status, opts);
307856
+ const cr = catalog_common.io.fromYaml(content);
307857
+ const itemPath = getCrItemPath(cr, opts.namespace);
307858
+ if (itemPath) {
307859
+ opts.prFileStatusByItemPath.set(itemPath, file.status);
307860
+ if (file.status !== FileStatus.DELETED) {
307861
+ opts.prResourceByItemPath.set(itemPath, cr);
307862
+ }
307863
+ }
307864
+ }
307865
+ catch {
307866
+ // Invalid YAML or unsupported resources are reported by planFile.
307867
+ }
307868
+ }
307869
+ }
307870
+ async function getPlanSkipReason(cr, status, opts) {
307871
+ if (!isPrRefStatus(status))
307872
+ return undefined;
307873
+ if (!GITHUB_REPOSITORY_DEPENDENT_KINDS.includes(cr.kind))
307874
+ return undefined;
307875
+ const repositoryRef = cr.spec?.repositoryTarget?.ref;
307876
+ if (repositoryRef?.kind !== 'FirestartrGithubRepository' ||
307877
+ !repositoryRef.name) {
307878
+ return undefined;
307879
+ }
307880
+ const namespace = cr.metadata?.namespace || opts.namespace;
307881
+ const repositoryItemPath = `${namespace}/${definitions_getPluralFromKind(repositoryRef.kind)}/${repositoryRef.name}`;
307882
+ if (opts.prFileStatusByItemPath.get(repositoryItemPath) !== FileStatus.ADDED) {
307883
+ return undefined;
307884
+ }
307885
+ const baseRepository = await getBaseGithubResource(repositoryItemPath, opts);
307886
+ if (baseRepository)
307887
+ return undefined;
307888
+ return (`Skipped plan: ${cr.kind}/${getResourceName(cr)} targets ` +
307889
+ `${repositoryRef.kind}/${repositoryRef.name}, which is created in this PR. ` +
307890
+ 'The repository feature and secrets section can only be planned after the repository exists.');
307891
+ }
307892
+ function getPrAwareItemByItemPath(opts) {
307893
+ return async (itemPath, apiGroup, apiVersion) => {
307894
+ if (opts.prFileStatusByItemPath.get(itemPath) === FileStatus.DELETED) {
307895
+ return undefined;
307896
+ }
307897
+ const prResource = opts.prResourceByItemPath.get(itemPath);
307898
+ if (prResource)
307899
+ return prResource;
307900
+ const baseResource = await getBaseGithubResource(itemPath, opts);
307901
+ if (baseResource)
307902
+ return baseResource;
307903
+ return getItemByItemPath(itemPath, apiGroup, apiVersion);
307904
+ };
307905
+ }
307906
+ async function getBaseGithubResource(itemPath, opts) {
307907
+ if (opts.baseResourceByItemPath.has(itemPath)) {
307908
+ return opts.baseResourceByItemPath.get(itemPath) ?? undefined;
307909
+ }
307910
+ const resourceFile = getGithubResourceFilename(itemPath);
307911
+ if (!resourceFile)
307912
+ return undefined;
307913
+ if (!opts.baseSha) {
307914
+ opts.baseSha = await github.pulls.getPrBaseSHA(opts.prNumber, opts.repo, opts.owner);
307915
+ }
307916
+ try {
307917
+ const content = await github.repo.getContent(resourceFile, opts.repo, opts.owner, opts.baseSha);
307918
+ const cr = catalog_common.io.fromYaml(content);
307919
+ opts.baseResourceByItemPath.set(itemPath, cr);
307920
+ return cr;
307921
+ }
307922
+ catch (e) {
307923
+ if (e.status === 404 || e.message?.includes('Not Found')) {
307924
+ opts.baseResourceByItemPath.set(itemPath, null);
307925
+ return undefined;
307926
+ }
307927
+ throw e;
307928
+ }
307929
+ }
307930
+ function getGithubResourceFilename(itemPath) {
307931
+ const [, plural, name] = itemPath.match(/[^/]+\/([^/]+)\/([^/]+)$/) || [];
307932
+ const kind = getKindFromPlural(plural);
307933
+ if (!kind || !GITHUB_RESOURCE_KINDS.includes(kind))
307934
+ return undefined;
307935
+ return `${kind}.${name}.yaml`;
307936
+ }
307937
+ function getCrItemPath(cr, namespace) {
307938
+ const plural = cr?.kind ? definitions_getPluralFromKind(cr.kind) : undefined;
307939
+ const name = getResourceName(cr);
307940
+ if (!plural || !name)
307941
+ return undefined;
307942
+ return `${cr.metadata?.namespace || namespace}/${plural}/${name}`;
307943
+ }
307944
+ function isPrRefStatus(status) {
307945
+ return (status === FileStatus.ADDED ||
307946
+ status === FileStatus.MODIFIED ||
307947
+ status === FileStatus.RENAMED ||
307948
+ status === FileStatus.COPIED ||
307949
+ status === FileStatus.CHANGED);
307950
+ }
307951
+ function isTrackedFileStatus(status) {
307952
+ return isPrRefStatus(status) || status === FileStatus.DELETED;
307953
+ }
307954
+ function getGithubPlanOptionsByStatus(status) {
307955
+ switch (status) {
307956
+ case FileStatus.MODIFIED:
307957
+ case FileStatus.ADDED:
307958
+ case FileStatus.RENAMED:
307959
+ case FileStatus.COPIED:
307960
+ case FileStatus.CHANGED:
307961
+ return { plan: true };
307962
+ case FileStatus.DELETED:
307963
+ return { planDestroy: true };
307964
+ default:
307965
+ throw new Error(`Unknown status: ${status}`);
307966
+ }
307967
+ }
307968
+ function skippedResult(filename, message) {
307969
+ return {
307970
+ filename,
307971
+ skipped: true,
307972
+ success: true,
307973
+ message,
307974
+ };
307975
+ }
307976
+ function isGithubResourceKind(kind) {
307977
+ return GITHUB_RESOURCE_KINDS.includes(kind);
307978
+ }
307979
+ function pull_request_plan_isYamlFile(filename) {
307980
+ return filename.endsWith('.yaml') || filename.endsWith('.yml');
307981
+ }
307982
+ function getResourceName(cr, fallbackName) {
307983
+ return cr.metadata?.name || cr.metadata?.generateName || fallbackName;
307984
+ }
307985
+ function withFallbackResourceName(cr, filename) {
307986
+ return {
307987
+ ...cr,
307988
+ metadata: {
307989
+ ...cr.metadata,
307990
+ generateName: cr.metadata?.generateName || filename,
307991
+ },
307992
+ };
307993
+ }
307994
+ function fallbackPlanResource(filename) {
307995
+ return {
307996
+ kind: 'UnknownKind',
307997
+ metadata: {
307998
+ name: filename,
307999
+ },
308000
+ };
308001
+ }
308002
+ function buildSummary(results) {
308003
+ const planned = results.filter((result) => !result.skipped);
308004
+ const failed = planned.filter((result) => !result.success);
308005
+ const skipped = results.filter((result) => result.skipped);
308006
+ const summaryLines = [
308007
+ failed.length > 0 ? 'Plan failed' : 'Plan completed',
308008
+ `Planned resources: ${planned.length}`,
308009
+ `Failed resources: ${failed.length}`,
308010
+ `Skipped files: ${skipped.length}`,
308011
+ ];
308012
+ const detailLines = [];
308013
+ for (const result of planned) {
308014
+ const resource = [result.kind, result.name].filter(Boolean).join('/');
308015
+ const status = result.success ? 'success' : 'failed';
308016
+ const details = result.success
308017
+ ? ''
308018
+ : ` (${summarizeFailure(result.message)})`;
308019
+ detailLines.push(`- ${status}: ${resource || result.filename}${details}`);
308020
+ }
308021
+ for (const result of skipped) {
308022
+ const resource = [result.kind, result.name].filter(Boolean).join('/');
308023
+ detailLines.push(`- skipped: ${resource || result.filename} (${result.message})`);
308024
+ }
308025
+ const lines = [...summaryLines, ''];
308026
+ lines.push(...detailLines.slice(0, SUMMARY_DETAIL_MAX_ITEMS));
308027
+ const omittedDetails = detailLines.length - SUMMARY_DETAIL_MAX_ITEMS;
308028
+ if (omittedDetails > 0) {
308029
+ lines.push(`- omitted detail lines: ${omittedDetails}`);
308030
+ }
308031
+ lines.push('', ...summaryLines);
308032
+ return lines.join('\n');
308033
+ }
308034
+ function summarizeFailure(message) {
308035
+ const [firstLine = ''] = message.split('\n');
308036
+ if (firstLine.length <= SUMMARY_FAILURE_MAX_LENGTH) {
308037
+ return firstLine;
308038
+ }
308039
+ return `${firstLine.slice(0, SUMMARY_FAILURE_MAX_LENGTH - 3)}...`;
308040
+ }
308041
+ function getCommandByStatus(status) {
308042
+ switch (status) {
308043
+ case FileStatus.MODIFIED:
308044
+ case FileStatus.ADDED:
308045
+ case FileStatus.RENAMED:
308046
+ case FileStatus.COPIED:
308047
+ case FileStatus.CHANGED:
308048
+ return 'plan';
308049
+ case FileStatus.DELETED:
308050
+ return 'plan-destroy';
308051
+ default:
308052
+ throw new Error(`Unknown status: ${status}`);
308053
+ }
308054
+ }
308055
+ function getSafeCommandByStatus(status) {
308056
+ try {
308057
+ return getCommandByStatus(status);
308058
+ }
308059
+ catch {
308060
+ return 'plan';
308061
+ }
308062
+ }
308063
+
307491
308064
  ;// CONCATENATED MODULE: ../operator/src/ctl_collections.ts
307492
308065
 
307493
308066
 
@@ -308413,7 +308986,7 @@ const operatorSubcommands = {
308413
308986
  });
308414
308987
  }
308415
308988
  if (options['pull-request-plan']) {
308416
- await tfWorkspacePlan({
308989
+ await pullRequestPlan({
308417
308990
  prNumber: parseInt(options['prNumber']),
308418
308991
  repo: options['repo'],
308419
308992
  owner: options['owner'],
@@ -308770,7 +309343,7 @@ const crs_analyzerSubcommand = {
308770
309343
  };
308771
309344
 
308772
309345
  ;// CONCATENATED MODULE: ./package.json
308773
- const package_namespaceObject = JSON.parse('{"i8":"2.4.0-snapshot-1"}');
309346
+ const package_namespaceObject = JSON.parse('{"i8":"2.4.0-snapshot-3"}');
308774
309347
  ;// CONCATENATED MODULE: ../../package.json
308775
309348
  const package_namespaceObject_1 = {"i8":"2.3.0"};
308776
309349
  ;// 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;
@@ -16,6 +16,7 @@ export default class RepoGithubDecanter extends GithubDecanter {
16
16
  __gatherOIDCSubjectClaim(): Promise<void>;
17
17
  __gatherBranchStrategy(): Promise<void>;
18
18
  __gatherCodeowners(): Promise<void>;
19
+ __gatherPagesConfiguration(): Promise<void>;
19
20
  __gatherRepoLabels(): Promise<void>;
20
21
  __adaptInitializerBranchStrategies(_claim: any): Promise<BranchStrategiesInitializer>;
21
22
  __adaptInitializerBase(_claim: any): Promise<InitializerDefault>;
@@ -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-1",
3
+ "version": "2.4.0-snapshot-3",
4
4
  "private": false,
5
5
  "description": "Commandline tool",
6
6
  "main": "build/main.js",