@firestartr/cli 1.53.0-snapshot-4 → 1.53.0-snapshot-6

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
@@ -354119,7 +354119,7 @@ async function getUserInfo(name) {
354119
354119
  });
354120
354120
 
354121
354121
  ;// CONCATENATED MODULE: ../github/src/sticky_comment.ts
354122
-
354122
+ const MAX_MULTIPART_COMMENTS = 100;
354123
354123
  const locks = new Map();
354124
354124
  async function withLock(key, fn) {
354125
354125
  const prev = locks.get(key) ?? Promise.resolve();
@@ -354140,116 +354140,147 @@ async function withLock(key, fn) {
354140
354140
  function escapeForRegex(s) {
354141
354141
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
354142
354142
  }
354143
- function prIdRegex(kind) {
354144
- return new RegExp(`<!-- sticky-id:${escapeForRegex(kind)}=(\\d+) -->`);
354143
+ function multiPartIdRegex(baseKind, index) {
354144
+ return new RegExp(`<!-- sticky-id:${escapeForRegex(baseKind)}\\[${index}\\]=(\\d+) -->`);
354145
354145
  }
354146
- function prIdMarker(kind, id) {
354147
- return `<!-- sticky-id:${kind}=${id} -->`;
354146
+ function multiPartIdMarker(baseKind, index, id) {
354147
+ return `<!-- sticky-id:${baseKind}[${index}]=${id} -->`;
354148
354148
  }
354149
- function bodyMarker(kind) {
354150
- return `<!-- sticky:kind=${kind} -->`;
354149
+ function multiPartBodyMarker(baseKind, index) {
354150
+ return `<!-- sticky:kind=${baseKind}[${index}] -->`;
354151
354151
  }
354152
- async function readStickyIdFromPrBody(octokit, owner, repo, pr, kind) {
354152
+ // ---- Helpers for reducing API calls and duplication ----
354153
+ async function getPrBody(octokit, owner, repo, pr) {
354153
354154
  const prResp = await octokit.rest.pulls.get({ owner, repo, pull_number: pr });
354154
- const body = prResp.data?.body ?? '';
354155
- const m = body.match(prIdRegex(kind));
354156
- github_src_logger.info(`readStickyIdFromPrBody: owner=${owner} repo=${repo} pr=${pr} kind=${kind} => id=${m ? m[1] : 'undefined'}`);
354157
- return m ? Number(m[1]) : undefined;
354158
- }
354159
- async function writeStickyIdToPrBody(octokit, owner, repo, pr, kind, id) {
354160
- const get = await octokit.rest.pulls.get({ owner, repo, pull_number: pr });
354161
- const body = get.data?.body ?? '';
354162
- const rx = prIdRegex(kind);
354163
- const marker = prIdMarker(kind, id);
354164
- github_src_logger.info(`writeStickyIdToPrBody: owner=${owner} repo=${repo} pr=${pr} kind=${kind} id=${id}`);
354165
- const next = rx.test(body)
354166
- ? body.replace(rx, marker)
354167
- : body
354168
- ? `${body}\n${marker}`
354169
- : marker;
354170
- if (next !== body) {
354155
+ return prResp.data?.body ?? '';
354156
+ }
354157
+ async function updatePrBodyIfChanged(octokit, owner, repo, pr, newBody, oldBody) {
354158
+ if (newBody !== oldBody) {
354171
354159
  await octokit.rest.pulls.update({
354172
354160
  owner,
354173
354161
  repo,
354174
354162
  pull_number: pr,
354175
- body: next,
354163
+ body: newBody,
354176
354164
  });
354177
354165
  }
354178
354166
  }
354179
- async function upsertStickyComment(octokit, params) {
354180
- const { owner, repo, pullNumber, kind } = params;
354181
- const fullBody = `${params.body}\n\n${bodyMarker(kind)}`;
354182
- const lockKey = `${owner}/${repo}#${pullNumber}#${kind}`;
354183
- await withLock(lockKey, async () => {
354184
- // 1) PR-body registry fast path
354185
- const idFromPr = await readStickyIdFromPrBody(octokit, owner, repo, pullNumber, kind);
354186
- if (idFromPr) {
354187
- github_src_logger.info(`upsertStickyComment: found existing comment ID ${idFromPr} from PR body for kind=${kind}`);
354188
- await octokit.rest.issues.updateComment({
354189
- owner,
354190
- repo,
354191
- comment_id: idFromPr,
354192
- body: fullBody,
354193
- });
354194
- return;
354167
+ async function updateComment(octokit, owner, repo, commentId, body) {
354168
+ await octokit.rest.issues.updateComment({
354169
+ owner,
354170
+ repo,
354171
+ comment_id: commentId,
354172
+ body,
354173
+ });
354174
+ }
354175
+ async function createCommentAndGetId(octokit, owner, repo, pullNumber, body) {
354176
+ const created = await octokit.rest.issues.createComment({
354177
+ owner,
354178
+ repo,
354179
+ issue_number: pullNumber,
354180
+ body,
354181
+ });
354182
+ return created?.data?.id;
354183
+ }
354184
+ async function readMultiPartStickyIdsFromPrBody(octokit, owner, repo, pr, baseKind) {
354185
+ const body = await getPrBody(octokit, owner, repo, pr);
354186
+ const ids = new Map();
354187
+ // Find all indexed markers for this base kind
354188
+ // Break early once we encounter a gap (most comments are sequentially indexed)
354189
+ let consecutiveGaps = 0;
354190
+ for (let i = 0; i < MAX_MULTIPART_COMMENTS; i++) {
354191
+ const m = body.match(multiPartIdRegex(baseKind, i));
354192
+ if (m) {
354193
+ ids.set(i, Number(m[1]));
354194
+ consecutiveGaps = 0;
354195
+ }
354196
+ else {
354197
+ consecutiveGaps++;
354198
+ // Stop after finding 5 consecutive gaps (reasonable buffer for sparse indices)
354199
+ if (consecutiveGaps >= 5)
354200
+ break;
354195
354201
  }
354196
- // 2) Try to find existing comments by kind marker
354197
- try {
354198
- const all = await octokit.paginate(octokit.rest.issues.listComments, {
354199
- owner,
354200
- repo,
354201
- issue_number: pullNumber,
354202
- per_page: 100,
354203
- });
354204
- const marker = bodyMarker(kind);
354205
- const matches = all.filter((c) => typeof c.body === 'string' && c.body.includes(marker));
354206
- if (matches.length > 0) {
354207
- // Sort by created_at to identify the oldest comment
354208
- const sorted = matches.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
354209
- const primaryComment = sorted[0];
354210
- // Update the primary (oldest) comment
354211
- github_src_logger.info(`upsertStickyComment: found existing comment ID ${primaryComment.id} by marker for kind=${kind}, updating it and deleting ${sorted.length - 1} duplicates`);
354212
- await octokit.rest.issues.updateComment({
354213
- owner,
354214
- repo,
354215
- comment_id: primaryComment.id,
354216
- body: fullBody,
354217
- });
354218
- // Store its ID in PR body
354219
- await writeStickyIdToPrBody(octokit, owner, repo, pullNumber, kind, primaryComment.id);
354220
- // Delete all duplicate comments (from index 1 onwards)
354221
- for (let i = 1; i < sorted.length; i++) {
354222
- github_src_logger.info(`upsertStickyComment: deleting duplicate comment ID ${sorted[i].id} for kind=${kind}`);
354223
- try {
354224
- await octokit.rest.issues.deleteComment({
354225
- owner,
354226
- repo,
354227
- comment_id: sorted[i].id,
354228
- });
354229
- }
354230
- catch (deleteErr) {
354231
- // Log but continue deleting others
354232
- github_src_logger.warn(`Failed to delete duplicate comment ${sorted[i].id} for ${kind}: ${deleteErr}`);
354233
- }
354202
+ }
354203
+ return ids;
354204
+ }
354205
+ async function writeMultiPartStickyIdsToPrBody(octokit, owner, repo, pr, baseKind, ids) {
354206
+ const oldBody = await getPrBody(octokit, owner, repo, pr);
354207
+ let body = oldBody;
354208
+ // Determine the highest index we need to clean up
354209
+ // Add a small buffer to catch any stragglers from previous runs
354210
+ const maxIndex = ids.size > 0 ? Math.max(...ids.keys()) + 5 : 5;
354211
+ // Remove all old markers for this base kind (only up to needed range)
354212
+ for (let i = 0; i <= maxIndex && i < MAX_MULTIPART_COMMENTS; i++) {
354213
+ const rx = multiPartIdRegex(baseKind, i);
354214
+ body = body.replace(rx, '');
354215
+ }
354216
+ // Add all new markers
354217
+ let markers = '';
354218
+ ids.forEach((id, index) => {
354219
+ markers += `${multiPartIdMarker(baseKind, index, id)}\n`;
354220
+ });
354221
+ // Clean up extra newlines
354222
+ markers = markers.trim();
354223
+ let next = body.trim();
354224
+ if (markers) {
354225
+ next = next ? `${next}\n${markers}` : markers;
354226
+ }
354227
+ await updatePrBodyIfChanged(octokit, owner, repo, pr, next, oldBody);
354228
+ }
354229
+ /**
354230
+ * Upsert sticky comments for one or more parts.
354231
+ * Always uses indexed kinds (e.g., 'logs[0]', 'logs[1]', etc.) for consistency.
354232
+ *
354233
+ * Behavior:
354234
+ * - Single part (bodies.length === 1): Uses indexed kind like 'logs[0]'
354235
+ * - Multiple parts (bodies.length > 1): Uses indexed kinds 'logs[0]', 'logs[1]', etc.
354236
+ * - Non-last parts include a note about continuation
354237
+ * - Unused parts (from previous runs) are updated with a placeholder message
354238
+ * - Never deletes comments
354239
+ */
354240
+ async function upsertMultiPartStickyComments(octokit, params) {
354241
+ const { owner, repo, pullNumber, baseKind, bodies } = params;
354242
+ // Always use indexed kinds (e.g., 'logs[0]', 'logs[1]', etc.)
354243
+ const lockKey = `${owner}/${repo}#${pullNumber}#${baseKind}[multi]`;
354244
+ const totalParts = bodies.length;
354245
+ await withLock(lockKey, async () => {
354246
+ // Read existing comment IDs for this base kind
354247
+ const existingIds = await readMultiPartStickyIdsFromPrBody(octokit, owner, repo, pullNumber, baseKind);
354248
+ const newIds = new Map();
354249
+ // Process each part of the new content
354250
+ for (let partIndex = 0; partIndex < totalParts; partIndex++) {
354251
+ const body = bodies[partIndex];
354252
+ const isLastPart = partIndex === totalParts - 1;
354253
+ // Build the full comment body with continuation notice for non-last parts
354254
+ let fullBody = body;
354255
+ if (!isLastPart) {
354256
+ fullBody += `\n\n📝 **Note:** This is part ${partIndex + 1} of ${totalParts}. See next comment for continuation.`;
354257
+ }
354258
+ fullBody += `\n\n${multiPartBodyMarker(baseKind, partIndex)}`;
354259
+ const existingId = existingIds.get(partIndex);
354260
+ if (existingId) {
354261
+ // Update existing comment
354262
+ await updateComment(octokit, owner, repo, existingId, fullBody);
354263
+ newIds.set(partIndex, existingId);
354264
+ }
354265
+ else {
354266
+ // Create new comment
354267
+ const newId = await createCommentAndGetId(octokit, owner, repo, pullNumber, fullBody);
354268
+ if (newId !== undefined) {
354269
+ newIds.set(partIndex, newId);
354234
354270
  }
354235
- return;
354236
354271
  }
354237
354272
  }
354238
- catch (err) {
354239
- // if paginate/listComments not available, we'll fall back to creating
354240
- github_src_logger.warn(`Failed to list comments for kind=${kind} on PR #${pullNumber}: ${err}`);
354241
- }
354242
- // 3) Create new comment and register
354243
- const created = await octokit.rest.issues.createComment({
354244
- owner,
354245
- repo,
354246
- issue_number: pullNumber,
354247
- body: fullBody,
354248
- });
354249
- const newId = created?.data?.id;
354250
- if (newId !== undefined) {
354251
- await writeStickyIdToPrBody(octokit, owner, repo, pullNumber, kind, newId);
354273
+ // Handle old comments that are no longer needed (when new content is shorter)
354274
+ // Update them with a placeholder message instead of deleting, but keep their IDs for potential reuse
354275
+ for (const [oldIndex, oldId] of existingIds) {
354276
+ if (!newIds.has(oldIndex)) {
354277
+ const placeholderBody = `⚠️ This comment is no longer in use as the log output has been reduced.\n\n${multiPartBodyMarker(baseKind, oldIndex)}`;
354278
+ await updateComment(octokit, owner, repo, oldId, placeholderBody);
354279
+ newIds.set(oldIndex, oldId); // Preserve ID in PR body for reuse
354280
+ }
354252
354281
  }
354282
+ // Update PR body with all comment IDs
354283
+ await writeMultiPartStickyIdsToPrBody(octokit, owner, repo, pullNumber, baseKind, newIds);
354253
354284
  });
354254
354285
  }
354255
354286
 
@@ -354263,12 +354294,12 @@ async function commentInPR(comment, pr_number, repo, owner = 'prefapp', stickyKi
354263
354294
  github_src_logger.info(`Commenting ${comment} in PR ${pr_number} of ${owner}/${repo}`);
354264
354295
  const octokit = await getOctokitForOrg(owner);
354265
354296
  if (stickyKind) {
354266
- await upsertStickyComment(octokit, {
354297
+ await upsertMultiPartStickyComments(octokit, {
354267
354298
  owner,
354268
354299
  repo,
354269
354300
  pullNumber: pr_number,
354270
- kind: stickyKind,
354271
- body: comment,
354301
+ baseKind: stickyKind,
354302
+ bodies: [comment],
354272
354303
  });
354273
354304
  }
354274
354305
  else {
@@ -354751,12 +354782,12 @@ class GithubCheckRun {
354751
354782
  return;
354752
354783
  const base = this.checkRunComment ?? '';
354753
354784
  const linkLine = base ? `${base}[here](${link})` : `[here](${link})`;
354754
- await upsertStickyComment(this.octokit, {
354785
+ await upsertMultiPartStickyComments(this.octokit, {
354755
354786
  owner: this.owner,
354756
354787
  repo: this.repo,
354757
354788
  pullNumber: this.pullNumber,
354758
- kind: `check-run:${this.name}`,
354759
- body: linkLine,
354789
+ baseKind: `check-run:${this.name}`,
354790
+ bodies: [linkLine],
354760
354791
  });
354761
354792
  }
354762
354793
  }
@@ -354854,6 +354885,7 @@ async function encryptRepoSecret(owner, repo, section, plaintextValue) {
354854
354885
 
354855
354886
 
354856
354887
 
354888
+
354857
354889
  /* harmony default export */ const github_0 = ({
354858
354890
  org: organization,
354859
354891
  repo: repository,
@@ -354870,6 +354902,7 @@ async function encryptRepoSecret(owner, repo, section, plaintextValue) {
354870
354902
  feedback: {
354871
354903
  createCheckRun: createCheckRun,
354872
354904
  CheckRun: CheckRun,
354905
+ upsertMultiPartStickyComments: upsertMultiPartStickyComments,
354873
354906
  },
354874
354907
  encryption: {
354875
354908
  encryptRepoSecret: encryptRepoSecret,
@@ -356774,6 +356807,10 @@ class SyncerInitializer extends InitializerPatches {
356774
356807
  const provider = helperCTX(ctx).provider;
356775
356808
  return claim.providers[provider].sync || {};
356776
356809
  }
356810
+ function policyInfo(ctx) {
356811
+ const provider = helperCTX(ctx).provider;
356812
+ return claim.providers[provider].policy;
356813
+ }
356777
356814
  return [
356778
356815
  {
356779
356816
  validate(cr) {
@@ -356804,21 +356841,23 @@ class SyncerInitializer extends InitializerPatches {
356804
356841
  }
356805
356842
  },
356806
356843
  apply(cr) {
356844
+ cr.metadata.annotations = cr.metadata.annotations || {};
356845
+ // Apply general policy annotation
356846
+ if (policyInfo(this)) {
356847
+ cr.metadata.annotations['firestartr.dev/policy'] =
356848
+ policyInfo(this);
356849
+ }
356807
356850
  if (syncInfo(this).enabled) {
356808
- cr.metadata.annotations = cr.metadata.annotations || {};
356809
356851
  cr.metadata.annotations['firestartr.dev/sync-enabled'] = 'true';
356810
356852
  if (syncInfo(this).period) {
356811
- cr.metadata.annotations = cr.metadata.annotations || {};
356812
356853
  cr.metadata.annotations['firestartr.dev/sync-period'] =
356813
356854
  syncInfo(this).period;
356814
356855
  }
356815
356856
  if (syncInfo(this).policy) {
356816
- cr.metadata.annotations = cr.metadata.annotations || {};
356817
356857
  cr.metadata.annotations['firestartr.dev/sync-policy'] =
356818
356858
  syncInfo(this).policy;
356819
356859
  }
356820
356860
  if (syncInfo(this).schedule) {
356821
- cr.metadata.annotations = cr.metadata.annotations || {};
356822
356861
  cr.metadata.annotations[SYNC_SCHED_ANNOTATION] =
356823
356862
  syncInfo(this).schedule;
356824
356863
  cr.metadata.annotations[SYNC_SCHED_TIMEZONE_ANNOTATION] =
@@ -357539,6 +357578,69 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
357539
357578
  },
357540
357579
  additionalProperties: false,
357541
357580
  },
357581
+ PolicyType: {
357582
+ $id: 'firestartr.dev://common/PolicyType',
357583
+ type: 'string',
357584
+ description: 'Policy for resource management',
357585
+ enum: [
357586
+ 'apply',
357587
+ 'create-only',
357588
+ 'create-update-only',
357589
+ 'full-control',
357590
+ 'observe',
357591
+ 'observe-only',
357592
+ ],
357593
+ },
357594
+ SyncConfig: {
357595
+ $id: 'firestartr.dev://common/SyncConfig',
357596
+ type: 'object',
357597
+ description: 'Sync configuration for resources',
357598
+ properties: {
357599
+ enabled: {
357600
+ type: 'boolean',
357601
+ description: 'Enable periodic sync operations',
357602
+ },
357603
+ period: {
357604
+ type: 'string',
357605
+ pattern: '^[0-9]+[smhd]$',
357606
+ description: 'Sync period (e.g., 1h, 30m, 5s). Must be enabled without schedule.',
357607
+ },
357608
+ schedule: {
357609
+ type: 'string',
357610
+ description: 'Cron schedule for sync operations. Must be enabled without period.',
357611
+ },
357612
+ schedule_timezone: {
357613
+ type: 'string',
357614
+ description: 'Timezone for cron schedule (e.g., UTC, America/New_York)',
357615
+ },
357616
+ policy: {
357617
+ type: 'string',
357618
+ description: 'Policy for sync operations (apply or observe)',
357619
+ },
357620
+ },
357621
+ additionalProperties: false,
357622
+ required: ['enabled'],
357623
+ oneOf: [
357624
+ {
357625
+ required: ['period'],
357626
+ },
357627
+ {
357628
+ required: ['schedule'],
357629
+ },
357630
+ {
357631
+ not: {
357632
+ anyOf: [
357633
+ {
357634
+ required: ['period'],
357635
+ },
357636
+ {
357637
+ required: ['schedule'],
357638
+ },
357639
+ ],
357640
+ },
357641
+ },
357642
+ ],
357643
+ },
357542
357644
  },
357543
357645
  });
357544
357646
 
@@ -357899,6 +358001,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
357899
358001
  {
357900
358002
  type: 'object',
357901
358003
  properties: {
358004
+ policy: {
358005
+ $ref: 'firestartr.dev://common/PolicyType',
358006
+ },
357902
358007
  privacy: {
357903
358008
  type: 'string',
357904
358009
  enum: ['closed', 'secret'],
@@ -357909,6 +358014,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
357909
358014
  org: {
357910
358015
  type: 'string',
357911
358016
  },
358017
+ sync: {
358018
+ $ref: 'firestartr.dev://common/SyncConfig',
358019
+ },
357912
358020
  },
357913
358021
  required: ['org', 'privacy'],
357914
358022
  },
@@ -357934,6 +358042,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
357934
358042
  {
357935
358043
  type: 'object',
357936
358044
  properties: {
358045
+ policy: {
358046
+ $ref: 'firestartr.dev://common/PolicyType',
358047
+ },
357937
358048
  role: {
357938
358049
  type: 'string',
357939
358050
  enum: ['admin', 'member'],
@@ -357941,6 +358052,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
357941
358052
  org: {
357942
358053
  type: 'string',
357943
358054
  },
358055
+ sync: {
358056
+ $ref: 'firestartr.dev://common/SyncConfig',
358057
+ },
357944
358058
  },
357945
358059
  required: ['org', 'role'],
357946
358060
  },
@@ -357965,6 +358079,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
357965
358079
  {
357966
358080
  type: 'object',
357967
358081
  properties: {
358082
+ policy: {
358083
+ $ref: 'firestartr.dev://common/PolicyType',
358084
+ },
357968
358085
  org: {
357969
358086
  type: 'string',
357970
358087
  description: 'The github organization name',
@@ -357973,6 +358090,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
357973
358090
  type: 'string',
357974
358091
  enum: ['private', 'public', 'internal'],
357975
358092
  },
358093
+ sync: {
358094
+ $ref: 'firestartr.dev://common/SyncConfig',
358095
+ },
357976
358096
  features: {
357977
358097
  type: 'array',
357978
358098
  items: {
@@ -358008,6 +358128,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
358008
358128
  {
358009
358129
  type: 'object',
358010
358130
  properties: {
358131
+ policy: {
358132
+ $ref: 'firestartr.dev://common/PolicyType',
358133
+ },
358011
358134
  orgName: {
358012
358135
  type: 'string',
358013
358136
  description: 'Organization name on GitHub',
@@ -358041,6 +358164,9 @@ const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createReq
358041
358164
  },
358042
358165
  required: ['url', 'contentType', 'events', 'secretRef'],
358043
358166
  },
358167
+ sync: {
358168
+ $ref: 'firestartr.dev://common/SyncConfig',
358169
+ },
358044
358170
  },
358045
358171
  required: ['orgName', 'webhook'],
358046
358172
  },
@@ -358212,15 +358338,7 @@ const GithubSchemas = [
358212
358338
  type: 'object',
358213
358339
  properties: {
358214
358340
  policy: {
358215
- type: 'string',
358216
- enum: [
358217
- 'apply',
358218
- 'create-only',
358219
- 'create-update-only',
358220
- 'full-control',
358221
- 'observe',
358222
- 'observe-only',
358223
- ],
358341
+ $ref: 'firestartr.dev://common/PolicyType',
358224
358342
  },
358225
358343
  name: {
358226
358344
  type: 'string',
@@ -358230,47 +358348,7 @@ const GithubSchemas = [
358230
358348
  enum: ['remote', 'inline', 'Remote', 'Inline'],
358231
358349
  },
358232
358350
  sync: {
358233
- type: 'object',
358234
- properties: {
358235
- enabled: {
358236
- type: 'boolean',
358237
- },
358238
- period: {
358239
- type: 'string',
358240
- pattern: '^[0-9]+[smhd]$',
358241
- },
358242
- schedule: {
358243
- type: 'string',
358244
- },
358245
- schedule_timezone: {
358246
- type: 'string',
358247
- },
358248
- policy: {
358249
- type: 'string',
358250
- },
358251
- },
358252
- additionalProperties: false,
358253
- required: ['enabled'],
358254
- oneOf: [
358255
- {
358256
- required: ['period'],
358257
- },
358258
- {
358259
- required: ['schedule'],
358260
- },
358261
- {
358262
- not: {
358263
- anyOf: [
358264
- {
358265
- required: ['period'],
358266
- },
358267
- {
358268
- required: ['schedule'],
358269
- },
358270
- ],
358271
- },
358272
- },
358273
- ],
358351
+ $ref: 'firestartr.dev://common/SyncConfig',
358274
358352
  },
358275
358353
  valuesSchema: {
358276
358354
  type: 'string',
@@ -370377,10 +370455,15 @@ class Resource {
370377
370455
  this.set('operation', operation);
370378
370456
  this.set('deps', deps);
370379
370457
  }
370380
- async run() {
370458
+ async run(options) {
370381
370459
  await this.preprocess();
370382
370460
  await this.synth();
370383
- await this.runTerraform();
370461
+ if (options?.planOnly) {
370462
+ await this.runTerraformPlanOnly();
370463
+ }
370464
+ else {
370465
+ await this.runTerraform();
370466
+ }
370384
370467
  await this.postprocess();
370385
370468
  if (this.logStream) {
370386
370469
  this.logStream.end();
@@ -370405,6 +370488,13 @@ class Resource {
370405
370488
  log(msg) {
370406
370489
  this.logFn(msg);
370407
370490
  }
370491
+ async runTerraformPlanOnly() {
370492
+ await this.onTFStreaming();
370493
+ let output = '';
370494
+ output += await terraformInit(this.get('main_artifact'), this.logStream);
370495
+ output += await terraformPlan(this.get('main_artifact'), this.logStream);
370496
+ this.set('output', output);
370497
+ }
370408
370498
  async runTerraform() {
370409
370499
  await this.onTFStreaming();
370410
370500
  let output = '';
@@ -370792,7 +370882,7 @@ async function runProvisioner(data, opts) {
370792
370882
  if ('logStreamCallbacksTF' in opts) {
370793
370883
  resource.setTFStreamLogs(opts['logStreamCallbacksTF']);
370794
370884
  }
370795
- await resource.run();
370885
+ await resource.run({ planOnly: opts.planOnly });
370796
370886
  return resource;
370797
370887
  }
370798
370888
  function createInstanceOf(entity, op, deps) {
@@ -370892,25 +370982,33 @@ async function tryPublishDestroy(item, destroyOutput) {
370892
370982
  return;
370893
370983
  }
370894
370984
  const dividedOutput = github_0.pulls.divideCommentIntoChunks(destroyOutput, 250);
370895
- let currentCommentNo = 1;
370896
- for (const commentContent of dividedOutput) {
370897
- const comment = `<h1>
370985
+ const commentBodies = dividedOutput.map((commentContent, index) => {
370986
+ const isMultiPart = dividedOutput.length > 1;
370987
+ const partIndicator = isMultiPart ? ` (Part ${index + 1})` : '';
370988
+ return `<h1>
370898
370989
  <img width="25" src="https://raw.githubusercontent.com/firestartr-pro/docs/refs/heads/main/logos/square-nobg.png"> Destroy Finished
370899
370990
  </h1>
370900
370991
  <p><b>TFWorkspace: </b>${item.metadata.name}</p>
370901
370992
 
370902
370993
  <details id=github>
370903
- <summary>DESTROY LOGS ${dividedOutput.length > 1 ? '(Part ' + currentCommentNo + ')' : ''}</summary>
370994
+ <summary>DESTROY LOGS${partIndicator}</summary>
370904
370995
 
370905
370996
  \`\`\`shell
370906
370997
  ${commentContent}
370907
370998
  \`\`\`
370908
370999
  </details>`;
370909
- operator_src_logger.debug(`The user feedback for item '${item.kind}/${item.metadata.name}' is being published as a comment on pull request '${lastPr.number}' for repository '${repo}' in organization '${org}'.`);
370910
- await github_0.pulls.commentInPR(comment, lastPr.number, repo, org, 'tfworkspace:destroy');
370911
- operator_src_logger.debug(`The user feedback for the '${item.kind}/${item.metadata.name}' destroy operation is being published as a comment on pull request '${lastPr.number}'.`);
370912
- currentCommentNo += 1;
370913
- }
371000
+ });
371001
+ operator_src_logger.debug(`The user feedback for item '${item.kind}/${item.metadata.name}' is being published as a comment on pull request '${lastPr.number}' for repository '${repo}' in organization '${org}'.`);
371002
+ // Get octokit instance for the org to use with upsertMultiPartStickyComments
371003
+ const octokit = await github_0.auth.getOctokitForOrg(org);
371004
+ await github_0.feedback.upsertMultiPartStickyComments(octokit, {
371005
+ owner: org,
371006
+ repo,
371007
+ pullNumber: lastPr.number,
371008
+ baseKind: 'tfworkspace:destroy',
371009
+ bodies: commentBodies,
371010
+ });
371011
+ operator_src_logger.debug(`The user feedback for the '${item.kind}/${item.metadata.name}' destroy operation has been published as a comment on pull request '${lastPr.number}'.`);
370914
371012
  }
370915
371013
  catch (e) {
370916
371014
  operator_src_logger.error(`An error occurred while publishing user feedback for item '${item.kind}/${item.metadata.name}': '${e}'.`);
@@ -370919,23 +371017,31 @@ ${commentContent}
370919
371017
  async function publishApply(item, applyOutput, kind) {
370920
371018
  const { prNumber, repo, org } = extractPrInfo(item);
370921
371019
  const dividedOutput = github_0.pulls.divideCommentIntoChunks(applyOutput, 250);
370922
- let currentCommentNo = 1;
370923
- for (const commentContent of dividedOutput) {
370924
- const comment = `<h1>
371020
+ const commentBodies = dividedOutput.map((commentContent, index) => {
371021
+ const isMultiPart = dividedOutput.length > 1;
371022
+ const partIndicator = isMultiPart ? ` (Part ${index + 1})` : '';
371023
+ return `<h1>
370925
371024
  <img width="25" src="https://raw.githubusercontent.com/firestartr-pro/docs/refs/heads/main/logos/square-nobg.png"> Apply Finished
370926
371025
  </h1>
370927
371026
  <p><b>${kind}: </b>${item.metadata.name}</p>
370928
371027
 
370929
371028
  <details id=github>
370930
- <summary>APPLY LOGS ${dividedOutput.length > 1 ? '(Part ' + currentCommentNo + ')' : ''}</summary>
371029
+ <summary>APPLY LOGS${partIndicator}</summary>
370931
371030
 
370932
371031
  \`\`\`shell
370933
371032
  ${commentContent}
370934
371033
  \`\`\`
370935
371034
  </details>`;
370936
- await github_0.pulls.commentInPR(comment, prNumber, repo, org, 'apply');
370937
- currentCommentNo += 1;
370938
- }
371035
+ });
371036
+ // Get octokit instance for the org to use with upsertMultiPartStickyComments
371037
+ const octokit = await github_0.auth.getOctokitForOrg(org);
371038
+ await github_0.feedback.upsertMultiPartStickyComments(octokit, {
371039
+ owner: org,
371040
+ repo,
371041
+ pullNumber: prNumber,
371042
+ baseKind: `apply:${kind}`,
371043
+ bodies: commentBodies,
371044
+ });
370939
371045
  }
370940
371046
  function tryCreateErrorSummary(title, errorMsg) {
370941
371047
  try {
@@ -370993,23 +371099,31 @@ ${message}
370993
371099
  async function publishPlan(item, planOutput, prNumber, repo, org) {
370994
371100
  try {
370995
371101
  const dividedOutput = github_0.pulls.divideCommentIntoChunks(planOutput, 250);
370996
- let currentCommentNo = 1;
370997
- for (const commentContent of dividedOutput) {
370998
- const comment = `<h1>
371102
+ const commentBodies = dividedOutput.map((commentContent, index) => {
371103
+ const isMultiPart = dividedOutput.length > 1;
371104
+ const partIndicator = isMultiPart ? ` (Part ${index + 1})` : '';
371105
+ return `<h1>
370999
371106
  <img width="25" src="https://raw.githubusercontent.com/firestartr-pro/docs/refs/heads/main/logos/square-nobg.png"> Plan Finished
371000
371107
  </h1>
371001
371108
  <p><b>TFWorkspace: </b>${item.metadata.name}</p>
371002
371109
 
371003
371110
  <details id=github>
371004
- <summary>PLAN LOGS ${dividedOutput.length > 1 ? '(Part ' + currentCommentNo + ')' : ''}</summary>
371111
+ <summary>PLAN LOGS${partIndicator}</summary>
371005
371112
 
371006
371113
  \`\`\`shell
371007
371114
  ${commentContent}
371008
371115
  \`\`\`
371009
371116
  </details>`;
371010
- await github_0.pulls.commentInPR(comment, prNumber, repo, org, 'tfworkspace:plan');
371011
- currentCommentNo += 1;
371012
- }
371117
+ });
371118
+ // Get octokit instance for the org to use with upsertMultiPartStickyComments
371119
+ const octokit = await github_0.auth.getOctokitForOrg(org);
371120
+ await github_0.feedback.upsertMultiPartStickyComments(octokit, {
371121
+ owner: org,
371122
+ repo,
371123
+ pullNumber: prNumber,
371124
+ baseKind: 'tfworkspace:plan',
371125
+ bodies: commentBodies,
371126
+ });
371013
371127
  }
371014
371128
  catch (e) {
371015
371129
  console.error(e);
@@ -371050,6 +371164,51 @@ function helperCreateCheckRunName(cmd, item) {
371050
371164
  return `${item.kind} - ${cmd}`;
371051
371165
  }
371052
371166
 
371167
+ ;// CONCATENATED MODULE: ../operator/src/utils/index.ts
371168
+ const secretRegex = /\$\{\{ secrets\.(.*?) \}\}/g;
371169
+ function replaceConfigSecrets(config, secrets) {
371170
+ for (const key in config) {
371171
+ if (typeof config[key] === 'object' && config[key] !== null) {
371172
+ // If the property is an object, call this function recursively
371173
+ replaceConfigSecrets(config[key], secrets);
371174
+ }
371175
+ else if (typeof config[key] === 'string') {
371176
+ // If the property is a string and its value is equal to secrets.something,
371177
+ // replace the value with the value of the 'something' key in the secrets object
371178
+ config[key] = config[key].replace(secretRegex, (_, group1) => {
371179
+ if (!secrets[group1]) {
371180
+ throw new Error(`Secret ${group1} not found in secrets`);
371181
+ }
371182
+ return secrets[group1];
371183
+ });
371184
+ }
371185
+ }
371186
+ return config;
371187
+ }
371188
+ function replaceInlineSecrets(inline, secrets) {
371189
+ if (typeof inline !== 'string' || !inline)
371190
+ return inline;
371191
+ let result = inline;
371192
+ result = result.replace(secretRegex, (_, group1) => {
371193
+ if (!secrets[group1]) {
371194
+ throw new Error(`Secret ${group1} not found in secrets`);
371195
+ }
371196
+ return secrets[group1];
371197
+ });
371198
+ return result;
371199
+ }
371200
+ /**
371201
+ * Retrieves a policy annotation value from a custom resource
371202
+ * @param item - The CR to get the policy from
371203
+ * @param annotation - The annotation key to retrieve
371204
+ * @returns The policy value, or undefined if not set
371205
+ */
371206
+ function getPolicy(item, annotation) {
371207
+ const policy = item.metadata.annotations && item.metadata.annotations[annotation];
371208
+ if (policy)
371209
+ return policy;
371210
+ }
371211
+
371053
371212
  ;// CONCATENATED MODULE: ../operator/cdktf.ts
371054
371213
 
371055
371214
 
@@ -371059,9 +371218,17 @@ function helperCreateCheckRunName(cmd, item) {
371059
371218
 
371060
371219
 
371061
371220
 
371221
+
371222
+ const cdktf_LAST_STATE_PR_ANNOTATION = 'firestartr.dev/last-state-pr';
371062
371223
  function processOperation(item, op, handler) {
371063
371224
  operator_src_logger.info(`Processing operation ${op} on ${item.kind}/${item.metadata?.name}`);
371064
371225
  try {
371226
+ const policy = getPolicy(item, 'firestartr.dev/policy');
371227
+ // If general policy is observe/observe-only, route to observe mode instead of apply
371228
+ if (!policy || policy === 'observe' || policy === 'observe-only') {
371229
+ operator_src_logger.info(`Policy is '${policy || 'not set (default)'}', routing to observe mode`);
371230
+ return cdktf_observe(item, op, handler);
371231
+ }
371065
371232
  switch (op) {
371066
371233
  case OperationType.UPDATED:
371067
371234
  return updated(item, op, handler);
@@ -371086,6 +371253,11 @@ function processOperation(item, op, handler) {
371086
371253
  throw e;
371087
371254
  }
371088
371255
  }
371256
+ async function* cdktf_observe(item, op, handler) {
371257
+ for await (const transition of doPlan(item, op, handler)) {
371258
+ yield transition;
371259
+ }
371260
+ }
371089
371261
  async function* created(item, op, handler) {
371090
371262
  for await (const transition of doApply(item, op, handler)) {
371091
371263
  yield transition;
@@ -371121,8 +371293,18 @@ async function* sync(item, op, handler) {
371121
371293
  status: 'False',
371122
371294
  message: 'Synth CDKTF',
371123
371295
  };
371124
- for await (const transition of doApply(item, op, handler)) {
371125
- yield transition;
371296
+ const syncPolicy = getPolicy(item, 'firestartr.dev/sync-policy');
371297
+ if (syncPolicy === 'apply') {
371298
+ operator_src_logger.info(`SYNC OPERATION: applying item ${item.metadata.name} with sync-policy=${syncPolicy}`);
371299
+ for await (const transition of doApply(item, op, handler)) {
371300
+ yield transition;
371301
+ }
371302
+ }
371303
+ else {
371304
+ operator_src_logger.info(`SYNC OPERATION: planning item ${item.metadata.name} with sync-policy=${syncPolicy || 'default (observe)'}`);
371305
+ for await (const transition of doPlan(item, op, handler)) {
371306
+ yield transition;
371307
+ }
371126
371308
  }
371127
371309
  yield {
371128
371310
  item,
@@ -371161,15 +371343,14 @@ async function* markedToDeletion(item, op, handler) {
371161
371343
  message: 'Destroying process started',
371162
371344
  };
371163
371345
  const deps = await handler.resolveReferences();
371164
- const annotation = 'firestartr.dev/last-state-pr';
371165
- const statePr = item?.metadata?.annotations?.[annotation];
371346
+ const statePr = item?.metadata?.annotations?.[cdktf_LAST_STATE_PR_ANNOTATION];
371166
371347
  const hasStatePr = typeof statePr === 'string' && statePr.trim().length > 0;
371167
371348
  if (!hasStatePr) {
371168
371349
  operator_src_logger.warn(`CR ${item?.kind ?? 'UnknownKind'}/${item?.metadata?.name ?? 'unknown'} ` +
371169
- `has no "${annotation}" annotation; skipping GitHub Check Runs (synth, terraform apply).`);
371350
+ `has no "${cdktf_LAST_STATE_PR_ANNOTATION}" annotation; skipping GitHub Check Runs (synth, terraform apply).`);
371170
371351
  }
371171
371352
  else {
371172
- operator_src_logger.debug(`CR ${item.kind}/${item.metadata.name} uses "${annotation}" = ${statePr}`);
371353
+ operator_src_logger.debug(`CR ${item.kind}/${item.metadata.name} uses "${cdktf_LAST_STATE_PR_ANNOTATION}" = ${statePr}`);
371173
371354
  }
371174
371355
  const destroyOutput = await provisioner.runProvisioner({
371175
371356
  mainCr: item,
@@ -371204,7 +371385,7 @@ async function* markedToDeletion(item, op, handler) {
371204
371385
  };
371205
371386
  await handler.finalize(handler.pluralKind, item.metadata.namespace, item, 'firestartr.dev/finalizer');
371206
371387
  await handler.writeTerraformOutputInTfResult(item, output);
371207
- if (item.metadata.annotations['firestartr.dev/last-state-pr'] || false) {
371388
+ if (item.metadata.annotations[cdktf_LAST_STATE_PR_ANNOTATION] || false) {
371208
371389
  await addDestroyCommitStatus(item, 'success', 'Destroy operation completed', `Terraform Destroy ${item.metadata.name}`);
371209
371390
  }
371210
371391
  void handler.success();
@@ -371285,15 +371466,14 @@ async function* doApply(item, op, handler) {
371285
371466
  }
371286
371467
  const deps = await handler.resolveReferences();
371287
371468
  operator_src_logger.info(`Item ${item.metadata.name} has the following dependencies: ${deps}`);
371288
- const annotation = 'firestartr.dev/last-state-pr';
371289
- const statePr = item?.metadata?.annotations?.[annotation];
371469
+ const statePr = item?.metadata?.annotations?.[cdktf_LAST_STATE_PR_ANNOTATION];
371290
371470
  const hasStatePr = typeof statePr === 'string' && statePr.trim().length > 0;
371291
371471
  if (!hasStatePr) {
371292
371472
  operator_src_logger.warn(`CR ${item?.kind ?? 'UnknownKind'}/${item?.metadata?.name ?? 'unknown'} ` +
371293
- `has no "${annotation}" annotation; skipping GitHub Check Runs (synth, terraform apply).`);
371473
+ `has no "${cdktf_LAST_STATE_PR_ANNOTATION}" annotation; skipping GitHub Check Runs (synth, terraform apply).`);
371294
371474
  }
371295
371475
  else {
371296
- operator_src_logger.debug(`CR ${item.kind}/${item.metadata.name} uses "${annotation}" = ${statePr}`);
371476
+ operator_src_logger.debug(`CR ${item.kind}/${item.metadata.name} uses "${cdktf_LAST_STATE_PR_ANNOTATION}" = ${statePr}`);
371297
371477
  }
371298
371478
  const applyOutput = await provisioner.runProvisioner({
371299
371479
  mainCr: item,
@@ -371389,6 +371569,219 @@ async function* doApply(item, op, handler) {
371389
371569
  }
371390
371570
  }
371391
371571
  }
371572
+ async function* doPlan(item, op, handler) {
371573
+ let checkRunCtl;
371574
+ try {
371575
+ cleanTerraformState();
371576
+ yield {
371577
+ item,
371578
+ reason: op,
371579
+ type: 'PLANNING',
371580
+ status: 'True',
371581
+ message: 'Planning process started',
371582
+ };
371583
+ const deps = await handler.resolveReferences();
371584
+ const statePr = item?.metadata?.annotations?.[cdktf_LAST_STATE_PR_ANNOTATION];
371585
+ const hasStatePr = typeof statePr === 'string' && statePr.trim().length > 0;
371586
+ if (!hasStatePr) {
371587
+ operator_src_logger.warn(`CR ${item?.kind ?? 'UnknownKind'}/${item?.metadata?.name ?? 'unknown'} ` +
371588
+ `has no "${cdktf_LAST_STATE_PR_ANNOTATION}" annotation; skipping GitHub Check Runs for plan.`);
371589
+ }
371590
+ else {
371591
+ operator_src_logger.debug(`CR ${item.kind}/${item.metadata.name} uses "${cdktf_LAST_STATE_PR_ANNOTATION}" = ${statePr}`);
371592
+ await addPlanStatusCheck(statePr, 'CDKTF plan in progress...');
371593
+ }
371594
+ // Run provisioner in plan-only mode
371595
+ const planResult = await provisioner.runProvisioner({ mainCr: item, deps }, {
371596
+ planOnly: true,
371597
+ delete: 'deletionTimestamp' in item.metadata,
371598
+ ...(hasStatePr
371599
+ ? {
371600
+ logStreamCallbacksCDKTF: {
371601
+ prepare: async () => {
371602
+ checkRunCtl = await GHCheckRun('synth', item);
371603
+ return checkRunCtl;
371604
+ },
371605
+ },
371606
+ logStreamCallbacksTF: {
371607
+ prepare: async () => {
371608
+ checkRunCtl = await GHCheckRun('plan', item);
371609
+ return checkRunCtl;
371610
+ },
371611
+ },
371612
+ }
371613
+ : {}),
371614
+ });
371615
+ const planOutput = planResult?.output || '';
371616
+ // Parse terraform plan output to detect changes
371617
+ // Handles multiple Terraform output formats and versions
371618
+ const hasChanges = detectPlanChanges(planOutput);
371619
+ if (hasChanges) {
371620
+ yield {
371621
+ item,
371622
+ reason: op,
371623
+ type: 'OUT_OF_SYNC',
371624
+ status: 'True',
371625
+ message: 'Plan has changes',
371626
+ };
371627
+ yield {
371628
+ item,
371629
+ reason: op,
371630
+ type: 'PROVISIONED',
371631
+ status: 'False',
371632
+ message: 'Plan has changes',
371633
+ };
371634
+ }
371635
+ else {
371636
+ yield {
371637
+ item,
371638
+ reason: op,
371639
+ type: 'OUT_OF_SYNC',
371640
+ status: 'False',
371641
+ message: 'Plan has no changes',
371642
+ };
371643
+ yield {
371644
+ item,
371645
+ reason: op,
371646
+ type: 'PROVISIONED',
371647
+ status: 'True',
371648
+ message: 'Plan has no changes',
371649
+ };
371650
+ }
371651
+ // Store plan details for later reference
371652
+ yield {
371653
+ item,
371654
+ reason: op,
371655
+ type: 'LAST_PLAN_DETAILS',
371656
+ status: 'Unknown',
371657
+ message: planOutput,
371658
+ };
371659
+ yield {
371660
+ item,
371661
+ reason: op,
371662
+ type: 'PLANNING',
371663
+ status: 'False',
371664
+ message: 'Planning process finished',
371665
+ };
371666
+ if (hasStatePr) {
371667
+ await addPlanStatusCheck(statePr, hasChanges ? 'Plan has changes' : 'Plan has no changes', 'completed');
371668
+ }
371669
+ }
371670
+ catch (e) {
371671
+ operator_src_logger.error(`CDKTF plan failed: ${e}`);
371672
+ if (checkRunCtl) {
371673
+ checkRunCtl.fnOnError(e);
371674
+ }
371675
+ yield {
371676
+ item,
371677
+ reason: op,
371678
+ type: 'ERROR',
371679
+ status: 'True',
371680
+ message: e.toString(),
371681
+ };
371682
+ yield {
371683
+ item,
371684
+ reason: op,
371685
+ type: 'PLANNING',
371686
+ status: 'False',
371687
+ message: e.toString(),
371688
+ };
371689
+ yield {
371690
+ item,
371691
+ reason: op,
371692
+ type: 'PROVISIONED',
371693
+ status: 'False',
371694
+ message: e.toString(),
371695
+ };
371696
+ const statePr = item?.metadata?.annotations?.[cdktf_LAST_STATE_PR_ANNOTATION];
371697
+ if (statePr) {
371698
+ const summaryText = tryCreateErrorSummary('CDKTF Plan failed', e);
371699
+ await addPlanStatusCheck(statePr, summaryText, 'completed', true);
371700
+ }
371701
+ await handler.writeTerraformOutputInTfResult(item, e);
371702
+ void handler.error();
371703
+ }
371704
+ }
371705
+ /**
371706
+ * Detects if a Terraform plan output contains changes
371707
+ * Handles multiple Terraform versions and output formats
371708
+ * @param planOutput - The text output from terraform plan
371709
+ * @returns true if changes are detected, false otherwise
371710
+ */
371711
+ function detectPlanChanges(planOutput) {
371712
+ if (!planOutput || planOutput.trim().length === 0) {
371713
+ return false;
371714
+ }
371715
+ // Normalize the output for consistent matching
371716
+ const normalized = planOutput.toLowerCase();
371717
+ // Pattern 1: "Plan: X to add, Y to change, Z to destroy"
371718
+ // Matches: "Plan: 1 to add, 0 to change, 0 to destroy"
371719
+ const planPattern = /plan:\s*(\d+)\s+to\s+add,\s*(\d+)\s+to\s+change,\s*(\d+)\s+to\s+destroy/i;
371720
+ const planMatch = planOutput.match(planPattern);
371721
+ if (planMatch) {
371722
+ const add = Number(planMatch[1]);
371723
+ const change = Number(planMatch[2]);
371724
+ const destroy = Number(planMatch[3]);
371725
+ if (add > 0 || change > 0 || destroy > 0) {
371726
+ return true;
371727
+ }
371728
+ // Explicitly found "Plan:" with 0/0/0 - no changes
371729
+ return false;
371730
+ }
371731
+ // Pattern 2: Individual change indicators
371732
+ // Handles variations like "1 to add", "2 to change", "3 to destroy"
371733
+ const hasAdditions = /\d+\s+to\s+add/i.test(planOutput);
371734
+ const hasChanges = /\d+\s+to\s+change/i.test(planOutput);
371735
+ const hasDestructions = /\d+\s+to\s+destroy/i.test(planOutput);
371736
+ const hasImports = /\d+\s+to\s+import/i.test(planOutput);
371737
+ if (hasAdditions || hasChanges || hasDestructions || hasImports) {
371738
+ return true;
371739
+ }
371740
+ // Pattern 3: Resource-level change indicators
371741
+ // Matches: "# resource will be created", "# resource will be updated", etc.
371742
+ const resourceChangePatterns = [
371743
+ /will\s+be\s+(created|destroyed|updated|replaced)/i,
371744
+ /must\s+be\s+(created|destroyed|updated|replaced)/i,
371745
+ /#.*\s+(create|destroy|update|replace)/i,
371746
+ ];
371747
+ for (const pattern of resourceChangePatterns) {
371748
+ if (pattern.test(planOutput)) {
371749
+ return true;
371750
+ }
371751
+ }
371752
+ // Pattern 4: Action symbols in plan output
371753
+ // Terraform uses symbols like +, -, ~, -/+ to indicate changes
371754
+ const actionSymbols = [
371755
+ /^\s*[+]\s+/m,
371756
+ /^\s*[-]\s+/m,
371757
+ /^\s*[~]\s+/m,
371758
+ /^\s*[-][/][+]\s+/m, // Replace
371759
+ ];
371760
+ for (const pattern of actionSymbols) {
371761
+ if (pattern.test(planOutput)) {
371762
+ return true;
371763
+ }
371764
+ }
371765
+ // Pattern 5: No changes messages (inverse check)
371766
+ const noChangesPatterns = [
371767
+ /no\s+changes/i,
371768
+ /infrastructure\s+is\s+up[-\s]to[-\s]date/i,
371769
+ /your\s+infrastructure\s+matches\s+the\s+configuration/i,
371770
+ /0\s+to\s+add,\s*0\s+to\s+change,\s*0\s+to\s+destroy/i,
371771
+ ];
371772
+ for (const pattern of noChangesPatterns) {
371773
+ if (pattern.test(planOutput)) {
371774
+ return false;
371775
+ }
371776
+ }
371777
+ // If we find "Plan:" keyword but couldn't parse it, log a warning and assume no changes
371778
+ if (normalized.includes('plan:')) {
371779
+ operator_src_logger.warn('Found "Plan:" in output but could not parse change counts. Assuming no changes.');
371780
+ return false;
371781
+ }
371782
+ // Default: assume no changes if we can't detect any
371783
+ return false;
371784
+ }
371392
371785
  function cleanTerraformState() {
371393
371786
  external_fs_.rmSync('/library/packages/provisioner/cdktf.out', {
371394
371787
  recursive: true,
@@ -372388,40 +372781,6 @@ function tf_checkrun_helperCreateCheckRunName(cmd) {
372388
372781
  return `TFWorkspace - ${cmd}`;
372389
372782
  }
372390
372783
 
372391
- ;// CONCATENATED MODULE: ../operator/src/utils/index.ts
372392
- const secretRegex = /\$\{\{ secrets\.(.*?) \}\}/g;
372393
- function replaceConfigSecrets(config, secrets) {
372394
- for (const key in config) {
372395
- if (typeof config[key] === 'object' && config[key] !== null) {
372396
- // If the property is an object, call this function recursively
372397
- replaceConfigSecrets(config[key], secrets);
372398
- }
372399
- else if (typeof config[key] === 'string') {
372400
- // If the property is a string and its value is equal to secrets.something,
372401
- // replace the value with the value of the 'something' key in the secrets object
372402
- config[key] = config[key].replace(secretRegex, (_, group1) => {
372403
- if (!secrets[group1]) {
372404
- throw new Error(`Secret ${group1} not found in secrets`);
372405
- }
372406
- return secrets[group1];
372407
- });
372408
- }
372409
- }
372410
- return config;
372411
- }
372412
- function replaceInlineSecrets(inline, secrets) {
372413
- if (typeof inline !== 'string' || !inline)
372414
- return inline;
372415
- let result = inline;
372416
- result = result.replace(secretRegex, (_, group1) => {
372417
- if (!secrets[group1]) {
372418
- throw new Error(`Secret ${group1} not found in secrets`);
372419
- }
372420
- return secrets[group1];
372421
- });
372422
- return result;
372423
- }
372424
-
372425
372784
  ;// CONCATENATED MODULE: ../operator/src/tfworkspaces/process-operation.ts
372426
372785
 
372427
372786
 
@@ -372612,11 +372971,6 @@ async function* process_operation_renamed(item, op, handler) {
372612
372971
  yield transition;
372613
372972
  }
372614
372973
  }
372615
- function getPolicy(item, annotation) {
372616
- const policy = item.metadata.annotations && item.metadata.annotations[annotation];
372617
- if (policy)
372618
- return policy;
372619
- }
372620
372974
  async function* process_operation_updated(item, op, handler) {
372621
372975
  for await (const transition of process_operation_doApply(item, op, handler)) {
372622
372976
  yield transition;
@@ -373260,7 +373614,7 @@ const processOperationPlan_TF_PROJECTS_PATH = '/tmp/tfworkspaces';
373260
373614
  function processOperationPlan(item, op, handler) {
373261
373615
  try {
373262
373616
  processOperationPlan_clearLocalTfProjects();
373263
- const policy = processOperationPlan_getPolicy(item);
373617
+ const policy = getPolicy(item, 'firestartr.dev/policy');
373264
373618
  if (policy === 'observe' || policy === 'apply') {
373265
373619
  return processOperationPlan_plan(item, op, handler);
373266
373620
  }
@@ -373707,12 +374061,6 @@ function processOperationPlan_getErrorOutputMessage(cr, key, ref) {
373707
374061
  throw new Error(`❌ Source ${cr.spec.source} not supported`);
373708
374062
  }
373709
374063
  }
373710
- function processOperationPlan_getPolicy(item) {
373711
- const policy = item.metadata.annotations &&
373712
- item.metadata.annotations['firestartr.dev/policy'];
373713
- if (policy)
373714
- return policy;
373715
- }
373716
374064
 
373717
374065
  ;// CONCATENATED MODULE: ../operator/src/ctx.ts
373718
374066
  class Ctx {