@catladder/cli 0.0.0-next-71c49b28 → 0.0.0-next-5dc1f907

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.
Files changed (85) hide show
  1. package/dist/bundles/catenv/index.js +75 -12
  2. package/dist/bundles/cli/index.js +847 -51
  3. package/dist/cli/src/apps/cli/commands/project/doctor/DoctorReport.d.ts +22 -0
  4. package/dist/cli/src/apps/cli/commands/project/doctor/DoctorReport.js +75 -0
  5. package/dist/cli/src/apps/cli/commands/project/doctor/DoctorReport.js.map +1 -0
  6. package/dist/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.d.ts +15 -0
  7. package/dist/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.js +59 -0
  8. package/dist/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.js.map +1 -0
  9. package/dist/cli/src/apps/cli/commands/project/doctor/checkCloudRun.d.ts +13 -0
  10. package/dist/cli/src/apps/cli/commands/project/doctor/checkCloudRun.js +120 -0
  11. package/dist/cli/src/apps/cli/commands/project/doctor/checkCloudRun.js.map +1 -0
  12. package/dist/cli/src/apps/cli/commands/project/doctor/checkGithub.d.ts +10 -0
  13. package/dist/cli/src/apps/cli/commands/project/doctor/checkGithub.js +100 -0
  14. package/dist/cli/src/apps/cli/commands/project/doctor/checkGithub.js.map +1 -0
  15. package/dist/cli/src/apps/cli/commands/project/doctor/checkGitlab.d.ts +13 -0
  16. package/dist/cli/src/apps/cli/commands/project/doctor/checkGitlab.js +79 -0
  17. package/dist/cli/src/apps/cli/commands/project/doctor/checkGitlab.js.map +1 -0
  18. package/dist/cli/src/apps/cli/commands/project/doctor/checkStore.d.ts +8 -0
  19. package/dist/cli/src/apps/cli/commands/project/doctor/checkStore.js +44 -0
  20. package/dist/cli/src/apps/cli/commands/project/doctor/checkStore.js.map +1 -0
  21. package/dist/cli/src/apps/cli/commands/project/doctor/index.d.ts +14 -0
  22. package/dist/cli/src/apps/cli/commands/project/doctor/index.js +48 -0
  23. package/dist/cli/src/apps/cli/commands/project/doctor/index.js.map +1 -0
  24. package/dist/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js +4 -1
  25. package/dist/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js.map +1 -1
  26. package/dist/cli/src/apps/cli/commands/project/setup/setupCloudRun.d.ts +21 -0
  27. package/dist/cli/src/apps/cli/commands/project/setup/setupCloudRun.js +31 -11
  28. package/dist/cli/src/apps/cli/commands/project/setup/setupCloudRun.js.map +1 -1
  29. package/dist/cli/src/commands/index.d.ts +1 -0
  30. package/dist/cli/src/commands/index.js +8 -1
  31. package/dist/cli/src/commands/index.js.map +1 -1
  32. package/dist/cli/src/commands/project/commandDoctor.d.ts +8 -0
  33. package/dist/cli/src/commands/project/commandDoctor.js +25 -0
  34. package/dist/cli/src/commands/project/commandDoctor.js.map +1 -0
  35. package/dist/cli/src/gcloud/artifactsRegistry.d.ts +1 -0
  36. package/dist/cli/src/gcloud/artifactsRegistry.js +3 -2
  37. package/dist/cli/src/gcloud/artifactsRegistry.js.map +1 -1
  38. package/dist/cli/src/gcloud/serviceAccountNames.d.ts +28 -0
  39. package/dist/cli/src/gcloud/serviceAccountNames.js +51 -0
  40. package/dist/cli/src/gcloud/serviceAccountNames.js.map +1 -0
  41. package/dist/cli/src/gcloud/serviceAccounts.js +6 -27
  42. package/dist/cli/src/gcloud/serviceAccounts.js.map +1 -1
  43. package/dist/cli/src/utils/concurrency.d.ts +7 -0
  44. package/dist/cli/src/utils/concurrency.js +27 -0
  45. package/dist/cli/src/utils/concurrency.js.map +1 -0
  46. package/dist/cli/src/utils/github.d.ts +10 -0
  47. package/dist/cli/src/utils/github.js +26 -1
  48. package/dist/cli/src/utils/github.js.map +1 -1
  49. package/dist/cli/src/utils/gitlab.d.ts +6 -0
  50. package/dist/cli/src/utils/gitlab.js +19 -1
  51. package/dist/cli/src/utils/gitlab.js.map +1 -1
  52. package/dist/pipeline/src/backends/github/createGithubJobs.js +24 -2
  53. package/dist/pipeline/src/backends/github/createGithubJobs.js.map +1 -1
  54. package/dist/pipeline/src/backends/gitlab/createGitlabJobs.js +8 -7
  55. package/dist/pipeline/src/backends/gitlab/createGitlabJobs.js.map +1 -1
  56. package/dist/pipeline/src/build/cache/createJobCache.js +8 -0
  57. package/dist/pipeline/src/build/cache/createJobCache.js.map +1 -1
  58. package/dist/pipeline/src/build/node/cache.js +15 -1
  59. package/dist/pipeline/src/build/node/cache.js.map +1 -1
  60. package/dist/pipeline/src/build/types.d.ts +19 -0
  61. package/dist/pipeline/src/build/types.js.map +1 -1
  62. package/dist/pipeline/src/constants.js +1 -1
  63. package/dist/pipeline/src/types/jobs.d.ts +6 -0
  64. package/dist/pipeline/src/types/jobs.js.map +1 -1
  65. package/dist/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +1 -1
  67. package/src/apps/cli/commands/project/doctor/DoctorReport.ts +72 -0
  68. package/src/apps/cli/commands/project/doctor/GcloudProjectInspector.ts +77 -0
  69. package/src/apps/cli/commands/project/doctor/checkCloudRun.ts +148 -0
  70. package/src/apps/cli/commands/project/doctor/checkGithub.ts +156 -0
  71. package/src/apps/cli/commands/project/doctor/checkGitlab.ts +133 -0
  72. package/src/apps/cli/commands/project/doctor/checkStore.ts +43 -0
  73. package/src/apps/cli/commands/project/doctor/index.ts +59 -0
  74. package/src/apps/cli/commands/project/setup/setupAccessTokens.ts +9 -2
  75. package/src/apps/cli/commands/project/setup/setupCloudRun.ts +42 -21
  76. package/src/commands/index.ts +1 -0
  77. package/src/commands/project/commandDoctor.ts +21 -0
  78. package/src/gcloud/__tests__/serviceAccountNames.test.ts +63 -0
  79. package/src/gcloud/artifactsRegistry.ts +3 -1
  80. package/src/gcloud/serviceAccountNames.ts +65 -0
  81. package/src/gcloud/serviceAccounts.ts +6 -38
  82. package/src/utils/concurrency.ts +25 -0
  83. package/src/utils/github.ts +48 -0
  84. package/src/utils/gitlab.ts +24 -0
  85. package/tsconfig.json +2 -1
@@ -587,6 +587,580 @@ exports.projectConfigSecrets = projectConfigSecrets;
587
587
 
588
588
  /***/ }),
589
589
 
590
+ /***/ 88089:
591
+ /***/ ((__unused_webpack_module, exports) => {
592
+
593
+ "use strict";
594
+
595
+
596
+ Object.defineProperty(exports, "__esModule", ({
597
+ value: true
598
+ }));
599
+ exports.DoctorReport = void 0;
600
+ /**
601
+ * collects the results of all doctor checks: each check logs
602
+ * immediately (progressive output like `project setup`) and problems
603
+ * are repeated in the final summary together with the command that
604
+ * heals them. The report itself never mutates anything — doctor
605
+ * diagnoses, setup heals.
606
+ */
607
+ class DoctorReport {
608
+ constructor(io) {
609
+ this.io = io;
610
+ this.passed = 0;
611
+ this.warnings = [];
612
+ this.problems = [];
613
+ this.currentSection = "";
614
+ }
615
+ section(title) {
616
+ this.currentSection = title;
617
+ this.io.log("");
618
+ this.io.log("🩺 " + title);
619
+ }
620
+ ok(message) {
621
+ this.passed++;
622
+ this.io.log(" ✅ " + message);
623
+ }
624
+ warn(message, remediation) {
625
+ this.warnings.push({
626
+ section: this.currentSection,
627
+ message,
628
+ remediation
629
+ });
630
+ this.io.log(" ⚠️ " + message);
631
+ if (remediation) this.io.log(" ↳ " + remediation);
632
+ }
633
+ fail(message, remediation) {
634
+ this.problems.push({
635
+ section: this.currentSection,
636
+ message,
637
+ remediation
638
+ });
639
+ this.io.log(" ❌ " + message);
640
+ if (remediation) this.io.log(" ↳ " + remediation);
641
+ }
642
+ get hasProblems() {
643
+ return this.problems.length > 0;
644
+ }
645
+ summarize() {
646
+ const {
647
+ io
648
+ } = this;
649
+ io.log("");
650
+ io.log("==================================================================================");
651
+ io.log(`🩺 doctor summary: ${this.passed} checks passed, ` + `${this.warnings.length} warnings, ${this.problems.length} problems`);
652
+ if (!this.hasProblems) {
653
+ io.log("");
654
+ io.log("everything looks healthy 😻");
655
+ return;
656
+ }
657
+ io.log("");
658
+ for (const {
659
+ section,
660
+ message,
661
+ remediation
662
+ } of this.problems) {
663
+ io.log(`❌ [${section}] ${message}`);
664
+ if (remediation) io.log(` ↳ ${remediation}`);
665
+ }
666
+ }
667
+ }
668
+ exports.DoctorReport = DoctorReport;
669
+
670
+ /***/ }),
671
+
672
+ /***/ 44952:
673
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
674
+
675
+ "use strict";
676
+
677
+
678
+ Object.defineProperty(exports, "__esModule", ({
679
+ value: true
680
+ }));
681
+ exports.GcloudProjectInspector = void 0;
682
+ const child_process_promise_1 = __nccwpck_require__(51748);
683
+ const artifactsRegistry_1 = __nccwpck_require__(41624);
684
+ /**
685
+ * read-only gcloud queries for the doctor, cached per project: checking
686
+ * many component/env contexts of the same gcloud project costs one
687
+ * `get-iam-policy` / `services list` call, not one per context
688
+ */
689
+ class GcloudProjectInspector {
690
+ constructor() {
691
+ this.iamBindings = new Map();
692
+ this.enabledServices = new Map();
693
+ }
694
+ async isAuthenticated() {
695
+ try {
696
+ await (0, child_process_promise_1.exec)("gcloud auth print-access-token");
697
+ return true;
698
+ } catch (_a) {
699
+ return false;
700
+ }
701
+ }
702
+ getIamBindings(projectId) {
703
+ if (!this.iamBindings.has(projectId)) {
704
+ this.iamBindings.set(projectId, (0, child_process_promise_1.exec)(`gcloud projects get-iam-policy ${projectId} --format=json`).then(result => {
705
+ var _a;
706
+ return (_a = JSON.parse(result.stdout).bindings) !== null && _a !== void 0 ? _a : [];
707
+ }));
708
+ }
709
+ return this.iamBindings.get(projectId);
710
+ }
711
+ /** all roles bound to the given member (e.g. "serviceAccount:<email>") */
712
+ async getRolesOfMember(projectId, member) {
713
+ const bindings = await this.getIamBindings(projectId);
714
+ return bindings.filter(binding => {
715
+ var _a;
716
+ return (_a = binding.members) === null || _a === void 0 ? void 0 : _a.includes(member);
717
+ }).map(binding => binding.role);
718
+ }
719
+ getEnabledServices(projectId) {
720
+ if (!this.enabledServices.has(projectId)) {
721
+ this.enabledServices.set(projectId, (0, child_process_promise_1.exec)(`gcloud services list --enabled --project=${projectId} --format="value(config.name)"`).then(result => new Set(result.stdout.split("\n").map(line => line.trim()).filter(Boolean))));
722
+ }
723
+ return this.enabledServices.get(projectId);
724
+ }
725
+ async hasArtifactsRegistry(projectId, region) {
726
+ try {
727
+ await (0, child_process_promise_1.exec)(`gcloud artifacts repositories describe ${artifactsRegistry_1.CATLADDER_REGISTRY_NAME} --project="${projectId}" --location=${region}`);
728
+ return true;
729
+ } catch (_a) {
730
+ return false;
731
+ }
732
+ }
733
+ }
734
+ exports.GcloudProjectInspector = GcloudProjectInspector;
735
+
736
+ /***/ }),
737
+
738
+ /***/ 65424:
739
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
740
+
741
+ "use strict";
742
+
743
+
744
+ Object.defineProperty(exports, "__esModule", ({
745
+ value: true
746
+ }));
747
+ exports.checkCloudRun = void 0;
748
+ const pipeline_1 = __nccwpck_require__(60241);
749
+ const artifactsRegistry_1 = __nccwpck_require__(41624);
750
+ const serviceAccountNames_1 = __nccwpck_require__(83798);
751
+ const serviceAccounts_1 = __nccwpck_require__(35611);
752
+ const concurrency_1 = __nccwpck_require__(5092);
753
+ const setupCloudRun_1 = __nccwpck_require__(51629);
754
+ const GcloudProjectInspector_1 = __nccwpck_require__(44952);
755
+ const getCloudRunConfig = context => {
756
+ var _a;
757
+ const config = (_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config;
758
+ if (!(0, pipeline_1.isOfDeployType)(config, "google-cloudrun")) {
759
+ throw new Error("not a cloud run context");
760
+ }
761
+ return config;
762
+ };
763
+ /**
764
+ * compares the gcloud state `setupCloudRun` would provision for the
765
+ * CURRENT config against what is actually there: enabled services and
766
+ * artifacts registry per project, deploy service account existence and
767
+ * IAM roles per component/env.
768
+ *
769
+ * The motivating case (#70): a component gets `cloudSql` after its
770
+ * setup ran — deploys keep working and only the review teardown hits
771
+ * the missing `roles/cloudsql.admin` as a 403 in ci.
772
+ */
773
+ const checkCloudRun = async (report, contexts) => {
774
+ var _a;
775
+ const cloudRunContexts = contexts.filter(context => {
776
+ var _a;
777
+ return (0, pipeline_1.isOfDeployType)((_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config, "google-cloudrun");
778
+ });
779
+ if (cloudRunContexts.length === 0) return;
780
+ report.section("google cloud run infrastructure");
781
+ const inspector = new GcloudProjectInspector_1.GcloudProjectInspector();
782
+ if (!(await inspector.isAuthenticated())) {
783
+ report.fail("gcloud is not authenticated — all google cloud checks skipped", "run: gcloud auth login");
784
+ return;
785
+ }
786
+ // project-level: enabled services + artifacts registry. The required
787
+ // services are the union over all components deploying to the project
788
+ // (cloudSql on any of them requires the sql services).
789
+ const byProject = new Map();
790
+ for (const context of cloudRunContexts) {
791
+ const {
792
+ projectId
793
+ } = getCloudRunConfig(context);
794
+ byProject.set(projectId, [...((_a = byProject.get(projectId)) !== null && _a !== void 0 ? _a : []), context]);
795
+ }
796
+ for (const [projectId, projectContexts] of byProject) {
797
+ const requiredServices = new Set(projectContexts.flatMap(context => (0, setupCloudRun_1.getCloudRunRequiredServices)(getCloudRunConfig(context))));
798
+ try {
799
+ const enabled = await inspector.getEnabledServices(projectId);
800
+ const missing = [...requiredServices].filter(service => !enabled.has(service));
801
+ if (missing.length === 0) {
802
+ report.ok(`${projectId}: all ${requiredServices.size} required services enabled`);
803
+ } else {
804
+ report.fail(`${projectId}: services not enabled: ${missing.join(", ")}`, "run: catladder project setup");
805
+ }
806
+ } catch (e) {
807
+ report.warn(`${projectId}: could not list enabled services (${e.message})`);
808
+ }
809
+ const regions = new Set(projectContexts.map(context => getCloudRunConfig(context).region));
810
+ for (const region of regions) {
811
+ if (await inspector.hasArtifactsRegistry(projectId, region)) {
812
+ report.ok(`${projectId}: artifacts registry '${artifactsRegistry_1.CATLADDER_REGISTRY_NAME}' exists in ${region}`);
813
+ } else {
814
+ report.fail(`${projectId}: artifacts registry '${artifactsRegistry_1.CATLADDER_REGISTRY_NAME}' missing in ${region}`, "run: catladder project setup");
815
+ }
816
+ }
817
+ }
818
+ // per component/env: deploy service account exists and carries every
819
+ // role the current config implies
820
+ const results = await (0, concurrency_1.mapWithConcurrency)(cloudRunContexts, 5, async context => {
821
+ const config = getCloudRunConfig(context);
822
+ const {
823
+ fullIdentifier
824
+ } = (0, serviceAccountNames_1.getGcloudServiceAccountNames)(context, {
825
+ name: setupCloudRun_1.CLOUD_RUN_DEPLOY_SA_NAME,
826
+ projectId: config.projectId
827
+ });
828
+ if (!(await (0, serviceAccounts_1.accountExists)(fullIdentifier))) {
829
+ return {
830
+ context,
831
+ fullIdentifier,
832
+ exists: false,
833
+ missingRoles: []
834
+ };
835
+ }
836
+ const boundRoles = await inspector.getRolesOfMember(config.projectId, `serviceAccount:${fullIdentifier}`);
837
+ const missingRoles = (0, setupCloudRun_1.getCloudRunDeployRoles)(config).filter(role => !boundRoles.includes(role));
838
+ return {
839
+ context,
840
+ fullIdentifier,
841
+ exists: true,
842
+ missingRoles
843
+ };
844
+ });
845
+ for (const {
846
+ context,
847
+ fullIdentifier,
848
+ exists,
849
+ missingRoles
850
+ } of results) {
851
+ const label = `${context.env}:${context.name}`;
852
+ if (!exists) {
853
+ report.fail(`${label}: deploy service account ${fullIdentifier} does not exist`, `run: catladder project setup ${context.name}`);
854
+ } else if (missingRoles.length > 0) {
855
+ report.fail(`${label}: service account is missing roles: ${missingRoles.join(", ")}`, `run: catladder project setup ${context.name}`);
856
+ } else {
857
+ report.ok(`${label}: service account exists with all config-implied roles`);
858
+ }
859
+ }
860
+ };
861
+ exports.checkCloudRun = checkCloudRun;
862
+
863
+ /***/ }),
864
+
865
+ /***/ 93265:
866
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
867
+
868
+ "use strict";
869
+
870
+
871
+ Object.defineProperty(exports, "__esModule", ({
872
+ value: true
873
+ }));
874
+ exports.checkGithub = void 0;
875
+ const pipeline_1 = __nccwpck_require__(60241);
876
+ const commandSecretsSyncGithub_1 = __nccwpck_require__(50962);
877
+ const concurrency_1 = __nccwpck_require__(5092);
878
+ const github_1 = __nccwpck_require__(18036);
879
+ /** marker for repo-level (environment-less) targets, see collectSecretTargets */
880
+ const REPO_LEVEL = "";
881
+ /**
882
+ * compares the secret/variable NAMES the generated github workflows
883
+ * reference against what is actually set — per github environment, the
884
+ * same mapping `project secrets-sync-github` writes. Values cannot be
885
+ * compared: github secrets are write-only by design (the vault stays
886
+ * the source of truth).
887
+ */
888
+ const checkGithub = async (report, config) => {
889
+ if (!(0, pipeline_1.getEnabledPipelineTypes)(config).includes("github")) return;
890
+ report.section("github environments & secrets");
891
+ if (!(await (0, github_1.isGhAuthenticated)())) {
892
+ report.warn("github cli (gh) not authenticated — github checks skipped", "run: gh auth login");
893
+ return;
894
+ }
895
+ const repo = await (0, commandSecretsSyncGithub_1.getConfiguredGithubRepo)();
896
+ if (!repo) {
897
+ report.fail(`no github repository found on the '${(0, pipeline_1.getPipelineGitRemote)(config, "github")}' remote`);
898
+ return;
899
+ }
900
+ let targets;
901
+ try {
902
+ targets = await (0, commandSecretsSyncGithub_1.collectSecretTargets)(config);
903
+ } catch (e) {
904
+ report.warn(`could not generate the github workflows to derive secret references (${e.message})`);
905
+ return;
906
+ }
907
+ const referencedNamesOf = environment => [...targets.entries()].filter(([, envs]) => envs.has(environment)).map(([name]) => name);
908
+ const environmentsNeeded = [...new Set([...targets.values()].flatMap(envs => [...envs]))].filter(environment => environment !== REPO_LEVEL).sort();
909
+ let existingEnvironments;
910
+ try {
911
+ existingEnvironments = new Set(await (0, github_1.listGithubEnvironments)(repo));
912
+ } catch (e) {
913
+ report.warn(`could not list environments of ${repo} (${e.message})`);
914
+ return;
915
+ }
916
+ const perEnvironment = await (0, concurrency_1.mapWithConcurrency)(environmentsNeeded, 8, async environment => {
917
+ const referenced = referencedNamesOf(environment);
918
+ if (!existingEnvironments.has(environment)) {
919
+ return {
920
+ environment,
921
+ referenced,
922
+ exists: false
923
+ };
924
+ }
925
+ const [secrets, variables] = await Promise.all([(0, github_1.listGithubSecretNames)(repo, environment), (0, github_1.listGithubVariableNames)(repo, environment)]);
926
+ // github secret names are case-insensitive and the api returns
927
+ // them uppercased — compare accordingly
928
+ const present = new Set([...secrets, ...variables].map(name => name.toUpperCase()));
929
+ const referencedUpper = new Set(referenced.map(name => name.toUpperCase()));
930
+ return {
931
+ environment,
932
+ referenced,
933
+ exists: true,
934
+ missing: referenced.filter(name => !present.has(name.toUpperCase())),
935
+ stale: [...present].filter(name => !referencedUpper.has(name))
936
+ };
937
+ });
938
+ for (const result of perEnvironment) {
939
+ const {
940
+ environment,
941
+ referenced
942
+ } = result;
943
+ if (!result.exists) {
944
+ report.fail(`environment '${environment}' does not exist (${referenced.length} referenced secrets have nowhere to live)`, "run: catladder project secrets-sync-github");
945
+ continue;
946
+ }
947
+ if (result.missing.length > 0) {
948
+ report.fail(`${environment}: missing ${result.missing.length}/${referenced.length} referenced secrets: ${result.missing.join(", ")}`, "run: catladder project secrets-sync-github");
949
+ } else {
950
+ report.ok(`${environment}: all ${referenced.length} referenced secrets/variables present`);
951
+ }
952
+ if (result.stale.length > 0) {
953
+ report.warn(`${environment}: ${result.stale.length} set but not referenced by any workflow (stale?): ${result.stale.join(", ")}`);
954
+ }
955
+ }
956
+ const repoLevelReferenced = referencedNamesOf(REPO_LEVEL);
957
+ if (repoLevelReferenced.length > 0) {
958
+ const [secrets, variables] = await Promise.all([(0, github_1.listGithubSecretNames)(repo), (0, github_1.listGithubVariableNames)(repo)]);
959
+ const present = new Set([...secrets, ...variables].map(name => name.toUpperCase()));
960
+ const missing = repoLevelReferenced.filter(name => !present.has(name.toUpperCase()));
961
+ if (missing.length > 0) {
962
+ report.fail(`repo level: missing ${missing.length}/${repoLevelReferenced.length} referenced secrets: ${missing.join(", ")}`, "run: catladder project secrets-sync-github");
963
+ } else {
964
+ report.ok(`repo level: all ${repoLevelReferenced.length} referenced secrets/variables present`);
965
+ }
966
+ }
967
+ };
968
+ exports.checkGithub = checkGithub;
969
+
970
+ /***/ }),
971
+
972
+ /***/ 47273:
973
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
974
+
975
+ "use strict";
976
+
977
+
978
+ Object.defineProperty(exports, "__esModule", ({
979
+ value: true
980
+ }));
981
+ exports.checkGitlab = void 0;
982
+ const pipeline_1 = __nccwpck_require__(60241);
983
+ const date_fns_1 = __nccwpck_require__(84022);
984
+ const gitlab_1 = __nccwpck_require__(4508);
985
+ const EXPIRY_WARNING_DAYS = 30;
986
+ /**
987
+ * the gitlab-side outputs of `project setup`: the semantic-release
988
+ * access token (+ its GL_TOKEN ci variable), the catladder topic and —
989
+ * for kubernetes deploys — the registry deploy token.
990
+ *
991
+ * Kubernetes namespace/rbac checks are deliberately skipped: they would
992
+ * require connecting kubectl to every cluster, which switches the local
993
+ * kubeconfig context — doctor must not change anything.
994
+ */
995
+ const checkGitlab = async (instance, report, contexts) => {
996
+ report.section("gitlab project setup");
997
+ let projectId;
998
+ try {
999
+ projectId = (await (0, gitlab_1.getProjectInfo)(instance)).id;
1000
+ } catch (e) {
1001
+ report.warn(`could not reach the gitlab api — gitlab checks skipped (${e.message})`);
1002
+ return;
1003
+ }
1004
+ try {
1005
+ const tokens = await (0, gitlab_1.doGitlabRequestAllPages)(instance, `projects/${projectId}/access_tokens`);
1006
+ const activeTokens = tokens.filter(t => t.name === "semantic-release" && t.active === true);
1007
+ // the GL_TOKEN ci variable holds exactly one of them — judge by the
1008
+ // one that lives longest
1009
+ const token = activeTokens.sort((a, b) => String(b.expires_at).localeCompare(String(a.expires_at)))[0];
1010
+ if (activeTokens.length > 1) {
1011
+ report.warn(`${activeTokens.length} active 'semantic-release' tokens exist — duplicates from earlier setups, consider revoking the old ones`);
1012
+ }
1013
+ if (!token) {
1014
+ report.fail("no active 'semantic-release' project access token", "run: catladder project setup");
1015
+ } else {
1016
+ const daysLeft = (0, date_fns_1.differenceInCalendarDays)(new Date(token.expires_at), new Date());
1017
+ if (daysLeft < 0) {
1018
+ report.fail(`'semantic-release' token expired on ${token.expires_at}`, "run: catladder project setup (rotates the token)");
1019
+ } else if (daysLeft <= EXPIRY_WARNING_DAYS) {
1020
+ report.warn(`'semantic-release' token expires in ${daysLeft} days (${token.expires_at})`, "run: catladder project setup (rotates the token)");
1021
+ } else {
1022
+ report.ok(`'semantic-release' token active (expires ${token.expires_at})`);
1023
+ }
1024
+ }
1025
+ try {
1026
+ await (0, gitlab_1.doGitlabRequest)(instance, `projects/${projectId}/variables/GL_TOKEN`);
1027
+ report.ok("GL_TOKEN ci variable present");
1028
+ } catch (_a) {
1029
+ report.fail("GL_TOKEN ci variable missing", "run: catladder project setup");
1030
+ }
1031
+ const {
1032
+ topics
1033
+ } = await (0, gitlab_1.doGitlabRequest)(instance, `projects/${projectId}`);
1034
+ if (topics === null || topics === void 0 ? void 0 : topics.includes("catladder")) {
1035
+ report.ok("'catladder' project topic set");
1036
+ } else {
1037
+ report.fail("'catladder' project topic missing", "run: catladder project setup");
1038
+ }
1039
+ if (contexts.some(context => {
1040
+ var _a;
1041
+ return (0, pipeline_1.isOfDeployType)((_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config, "kubernetes");
1042
+ })) {
1043
+ const deployTokens = await (0, gitlab_1.doGitlabRequestAllPages)(instance, `projects/${projectId}/deploy_tokens`);
1044
+ if (deployTokens.find(t => t.name === "gitlab-deploy-token")) {
1045
+ report.ok("'gitlab-deploy-token' registry deploy token present");
1046
+ } else {
1047
+ report.fail("'gitlab-deploy-token' registry deploy token missing (kubernetes cannot pull images)", "run: catladder project setup");
1048
+ }
1049
+ }
1050
+ } catch (e) {
1051
+ report.warn(`gitlab checks aborted (${e.message})`);
1052
+ }
1053
+ };
1054
+ exports.checkGitlab = checkGitlab;
1055
+
1056
+ /***/ }),
1057
+
1058
+ /***/ 21141:
1059
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
1060
+
1061
+ "use strict";
1062
+
1063
+
1064
+ Object.defineProperty(exports, "__esModule", ({
1065
+ value: true
1066
+ }));
1067
+ exports.checkStore = void 0;
1068
+ const pipeline_1 = __nccwpck_require__(60241);
1069
+ const getProjectConfig_1 = __nccwpck_require__(50001);
1070
+ const projects_1 = __nccwpck_require__(57414);
1071
+ /**
1072
+ * every google-cloudrun project in the config needs its project number
1073
+ * in the committed store — the deterministic cloud run urls
1074
+ * (<service>-<projectNumber>.<region>.run.app) are computed from it at
1075
+ * generation time. This check is fully offline.
1076
+ */
1077
+ const checkStore = async report => {
1078
+ var _a, _b, _c;
1079
+ const config = await (0, getProjectConfig_1.getProjectConfig)();
1080
+ const projectIds = new Set();
1081
+ for (const {
1082
+ componentName,
1083
+ env
1084
+ } of await (0, getProjectConfig_1.getAllComponentsWithAllEnvsFlat)()) {
1085
+ const {
1086
+ deploy
1087
+ } = (0, pipeline_1.getEnvConfig)(config, componentName, env);
1088
+ if (deploy && deploy.type === "google-cloudrun") {
1089
+ projectIds.add(deploy.projectId);
1090
+ }
1091
+ }
1092
+ if (projectIds.size === 0) return;
1093
+ report.section("catladder store (.catladder-store/store.yml)");
1094
+ const root = (_a = await (0, projects_1.getGitRoot)()) !== null && _a !== void 0 ? _a : process.cwd();
1095
+ const store = (0, pipeline_1.readCatladderStore)(root);
1096
+ for (const projectId of projectIds) {
1097
+ const projectNumber = (_c = (_b = store.gcloudProjects) === null || _b === void 0 ? void 0 : _b[projectId]) === null || _c === void 0 ? void 0 : _c.projectNumber;
1098
+ if (projectNumber) {
1099
+ report.ok(`project number for ${projectId}: ${projectNumber}`);
1100
+ } else {
1101
+ report.fail(`missing project number for ${projectId}`, "run: catladder project setup");
1102
+ }
1103
+ }
1104
+ };
1105
+ exports.checkStore = checkStore;
1106
+
1107
+ /***/ }),
1108
+
1109
+ /***/ 96270:
1110
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
1111
+
1112
+ "use strict";
1113
+
1114
+
1115
+ Object.defineProperty(exports, "__esModule", ({
1116
+ value: true
1117
+ }));
1118
+ exports.doctorProject = void 0;
1119
+ const pipeline_1 = __nccwpck_require__(60241);
1120
+ const getProjectConfig_1 = __nccwpck_require__(50001);
1121
+ const checkCloudRun_1 = __nccwpck_require__(65424);
1122
+ const checkGithub_1 = __nccwpck_require__(93265);
1123
+ const checkGitlab_1 = __nccwpck_require__(47273);
1124
+ const checkStore_1 = __nccwpck_require__(21141);
1125
+ const DoctorReport_1 = __nccwpck_require__(88089);
1126
+ /**
1127
+ * read-only health check: compares the state `project setup` (and
1128
+ * `project secrets-sync-github`) would provision for the CURRENT config
1129
+ * against what is actually there, and reports drift together with the
1130
+ * command that heals it. `project setup` is a point-in-time
1131
+ * provisioner, so config edits that change required infrastructure
1132
+ * (IAM roles, services, secrets) drift silently until something breaks
1133
+ * in ci — the doctor surfaces that before it does.
1134
+ *
1135
+ * The component filter only narrows the per-component checks; the
1136
+ * cheap project-global checks (store, gitlab, github) always run.
1137
+ */
1138
+ const doctorProject = async (instance, onlyComponents) => {
1139
+ const config = await (0, getProjectConfig_1.getProjectConfig)();
1140
+ if (!config) {
1141
+ throw new Error("no catladder config found");
1142
+ }
1143
+ instance.log("🐱 🩺 checking project health — read-only, nothing will be changed");
1144
+ const report = new DoctorReport_1.DoctorReport(instance);
1145
+ await (0, checkStore_1.checkStore)(report);
1146
+ let contexts = [];
1147
+ try {
1148
+ contexts = await (0, getProjectConfig_1.getAllPipelineContexts)(onlyComponents);
1149
+ } catch (e) {
1150
+ report.section("pipeline contexts");
1151
+ report.fail(`could not create pipeline contexts: ${e.message}`, "run: catladder project setup");
1152
+ }
1153
+ if ((0, pipeline_1.getEnabledPipelineTypes)(config).includes("gitlab")) {
1154
+ await (0, checkGitlab_1.checkGitlab)(instance, report, contexts);
1155
+ }
1156
+ await (0, checkCloudRun_1.checkCloudRun)(report, contexts);
1157
+ await (0, checkGithub_1.checkGithub)(report, config);
1158
+ report.summarize();
1159
+ };
1160
+ exports.doctorProject = doctorProject;
1161
+
1162
+ /***/ }),
1163
+
590
1164
  /***/ 22661:
591
1165
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
592
1166
 
@@ -746,7 +1320,10 @@ const setupAccessTokens = async instance => {
746
1320
  const {
747
1321
  id: projectId
748
1322
  } = await (0, gitlab_1.getProjectInfo)(instance);
749
- const projectTokens = await (0, gitlab_1.doGitlabRequest)(instance, `projects/${projectId}/access_tokens`);
1323
+ // all pages: rotated tokens accumulate one (inactive) entry per
1324
+ // rotation, so the active one quickly moves past the first page —
1325
+ // missing it here created a brand-new duplicate token on every setup
1326
+ const projectTokens = await (0, gitlab_1.doGitlabRequestAllPages)(instance, `projects/${projectId}/access_tokens`);
750
1327
  const token = projectTokens.find(t => t.name === TOKEN_NAME && t.active === true);
751
1328
  const expires_at = (0, date_fns_1.format)((0, date_fns_1.add)(new Date(), {
752
1329
  years: 1,
@@ -963,13 +1540,40 @@ exports.setupAgents = setupAgents;
963
1540
  Object.defineProperty(exports, "__esModule", ({
964
1541
  value: true
965
1542
  }));
966
- exports.setupCloudRun = void 0;
1543
+ exports.setupCloudRun = exports.getCloudRunDeployRoles = exports.getCloudRunRequiredServices = exports.CLOUD_RUN_DEPLOY_SA_NAME = void 0;
967
1544
  const pipeline_1 = __nccwpck_require__(60241);
968
1545
  const artifactsRegistry_1 = __nccwpck_require__(41624);
969
1546
  const enableServices_1 = __nccwpck_require__(96761);
970
1547
  const getProjectNumber_1 = __nccwpck_require__(68666);
971
1548
  const serviceAccounts_1 = __nccwpck_require__(35611);
972
1549
  const updateCatladderStore_1 = __nccwpck_require__(66487);
1550
+ exports.CLOUD_RUN_DEPLOY_SA_NAME = "cl-d";
1551
+ /**
1552
+ * the gcloud services the given deploy config requires.
1553
+ *
1554
+ * Shared with `project doctor`: setup enables them, doctor compares
1555
+ * them against what is actually enabled — config edits after setup
1556
+ * (e.g. adding `cloudSql`) would otherwise drift silently.
1557
+ */
1558
+ const getCloudRunRequiredServices = config => ["run.googleapis.com", "artifactregistry.googleapis.com", "cloudscheduler.googleapis.com",
1559
+ // for scheduling jobs
1560
+ "cloudresourcemanager.googleapis.com",
1561
+ // only required to get google cloud project number
1562
+ ...(config.cloudSql ? ["sqladmin.googleapis.com", "sql-component.googleapis.com"] : [])];
1563
+ exports.getCloudRunRequiredServices = getCloudRunRequiredServices;
1564
+ /**
1565
+ * the IAM roles the deploy service account needs for the given deploy
1566
+ * config. Shared with `project doctor` (same rationale as
1567
+ * {@link getCloudRunRequiredServices}) — the motivating case: a
1568
+ * component gets `cloudSql` after its setup ran, deploys keep working,
1569
+ * and only the review teardown hits the missing `cloudsql.admin`.
1570
+ */
1571
+ const getCloudRunDeployRoles = config => [
1572
+ // TODO: run.admin is a bit much, would be better to follow https://stackoverflow.com/questions/55788714/deploying-to-cloud-run-with-a-custom-service-account-failed-with-iam-serviceacco
1573
+ "roles/artifactregistry.repoAdmin", "roles/run.admin", "roles/iam.serviceAccountUser", "roles/cloudscheduler.admin",
1574
+ // for scheduling
1575
+ ...(config.cloudSql ? ["roles/cloudsql.admin"] : [])];
1576
+ exports.getCloudRunDeployRoles = getCloudRunDeployRoles;
973
1577
  const setupCloudRun = async (instance, context) => {
974
1578
  var _a, _b;
975
1579
  if (!(0, pipeline_1.isOfDeployType)((_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config, "google-cloudrun")) {
@@ -978,11 +1582,7 @@ const setupCloudRun = async (instance, context) => {
978
1582
  const config = (_b = context.deploy) === null || _b === void 0 ? void 0 : _b.config;
979
1583
  // enable services
980
1584
  instance.log("enable required services...");
981
- await (0, enableServices_1.enableGCloudServices)(["run.googleapis.com", "artifactregistry.googleapis.com", "cloudscheduler.googleapis.com",
982
- // for scheduling jobs
983
- "cloudresourcemanager.googleapis.com",
984
- // only required to get google cloud project number
985
- ...(config.cloudSql ? ["sqladmin.googleapis.com", "sql-component.googleapis.com"] : [])], config);
1585
+ await (0, enableServices_1.enableGCloudServices)((0, exports.getCloudRunRequiredServices)(config), config);
986
1586
  instance.log("upsert artifacts registry...");
987
1587
  await (0, artifactsRegistry_1.upsertGcloudArtifactsRegistry)(config);
988
1588
  instance.log("upsert deploy service account...");
@@ -990,11 +1590,8 @@ const setupCloudRun = async (instance, context) => {
990
1590
  projectId: config.projectId,
991
1591
  displayName: "Catladder gcloud deploy",
992
1592
  description: "This service account deploys to google cloud",
993
- name: "cl-d",
994
- // TODO: run.admin is a bit much, would be better to follow https://stackoverflow.com/questions/55788714/deploying-to-cloud-run-with-a-custom-service-account-failed-with-iam-serviceacco
995
- roles: ["roles/artifactregistry.repoAdmin", "roles/run.admin", "roles/iam.serviceAccountUser", "roles/cloudscheduler.admin",
996
- // for scheduling
997
- ...(config.cloudSql ? ["roles/cloudsql.admin"] : [])]
1593
+ name: exports.CLOUD_RUN_DEPLOY_SA_NAME,
1594
+ roles: (0, exports.getCloudRunDeployRoles)(config)
998
1595
  }, pipeline_1.GCLOUD_DEPLOY_CREDENTIALS_KEY);
999
1596
  // the project number is needed at generation time to construct cloud
1000
1597
  // run's deterministic urls (<service>-<projectNumber>.<region>.run.app)
@@ -2064,7 +2661,7 @@ exports.commandKubeStopPortforward = (0, defineCommand_1.defineCommand)({
2064
2661
  Object.defineProperty(exports, "__esModule", ({
2065
2662
  value: true
2066
2663
  }));
2067
- exports.commandStarwars = exports.commandDadjoke = exports.commandSecurityAuditCreate = exports.commandSecurityAuditEvaluate = exports.commandSecurityAuditCiJob = exports.commandRestoreDb = exports.commandMongoDestroyMember = exports.commandMongoPortForward = exports.commandMongoGetShell = exports.commandTriggerCronjobProject = exports.commandDeleteProject = exports.commandPauseProject = exports.commandDeletePods = exports.commandGetShell = exports.commandListPods = exports.commandNamespace = exports.commandSecurityEvaluate = exports.commandProjectRestoreDb = exports.commandCiJobLog = exports.commandCiJobOpen = exports.commandSetup = exports.commandGetMyTotalWorktime = exports.commandRenewToken = exports.commandTriggerCronjobGeneral = exports.commandOpenLogs = exports.commandSecretsSyncGithub = exports.commandSecretsClearBackups = exports.commandConfigSecrets = exports.commandCloudSqlProxy = exports.commandOpenGit = exports.commandOpenEnv = exports.commandPortForward = exports.commandEnvVars = exports.commandKubePortForward = exports.commandKubeGetShell = exports.commandKubeStopPortforward = exports.commandKubeListPods = exports.commandKubeListSecrets = exports.commandKubeListNamespaces = exports.commandKubeCurrentContext = void 0;
2664
+ exports.commandStarwars = exports.commandDadjoke = exports.commandSecurityAuditCreate = exports.commandSecurityAuditEvaluate = exports.commandSecurityAuditCiJob = exports.commandRestoreDb = exports.commandMongoDestroyMember = exports.commandMongoPortForward = exports.commandMongoGetShell = exports.commandTriggerCronjobProject = exports.commandDeleteProject = exports.commandPauseProject = exports.commandDeletePods = exports.commandGetShell = exports.commandListPods = exports.commandNamespace = exports.commandSecurityEvaluate = exports.commandProjectRestoreDb = exports.commandCiJobLog = exports.commandCiJobOpen = exports.commandDoctor = exports.commandSetup = exports.commandGetMyTotalWorktime = exports.commandRenewToken = exports.commandTriggerCronjobGeneral = exports.commandOpenLogs = exports.commandSecretsSyncGithub = exports.commandSecretsClearBackups = exports.commandConfigSecrets = exports.commandCloudSqlProxy = exports.commandOpenGit = exports.commandOpenEnv = exports.commandPortForward = exports.commandEnvVars = exports.commandKubePortForward = exports.commandKubeGetShell = exports.commandKubeStopPortforward = exports.commandKubeListPods = exports.commandKubeListSecrets = exports.commandKubeListNamespaces = exports.commandKubeCurrentContext = void 0;
2068
2665
  // General commands
2069
2666
  var commandKubeCurrentContext_1 = __nccwpck_require__(70243);
2070
2667
  Object.defineProperty(exports, "commandKubeCurrentContext", ({
@@ -2207,6 +2804,13 @@ Object.defineProperty(exports, "commandSetup", ({
2207
2804
  return commandSetup_1.commandSetup;
2208
2805
  }
2209
2806
  }));
2807
+ var commandDoctor_1 = __nccwpck_require__(80248);
2808
+ Object.defineProperty(exports, "commandDoctor", ({
2809
+ enumerable: true,
2810
+ get: function () {
2811
+ return commandDoctor_1.commandDoctor;
2812
+ }
2813
+ }));
2210
2814
  var commandGitlabCi_1 = __nccwpck_require__(57076);
2211
2815
  Object.defineProperty(exports, "commandCiJobOpen", ({
2212
2816
  enumerable: true,
@@ -2750,6 +3354,38 @@ exports.commandConfigSecrets = (0, defineCommand_1.defineCommand)({
2750
3354
 
2751
3355
  /***/ }),
2752
3356
 
3357
+ /***/ 80248:
3358
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
3359
+
3360
+ "use strict";
3361
+
3362
+
3363
+ Object.defineProperty(exports, "__esModule", ({
3364
+ value: true
3365
+ }));
3366
+ exports.commandDoctor = void 0;
3367
+ const doctor_1 = __nccwpck_require__(96270);
3368
+ const defineCommand_1 = __nccwpck_require__(39489);
3369
+ exports.commandDoctor = (0, defineCommand_1.defineCommand)({
3370
+ name: "project doctor",
3371
+ description: "Checks for drift between the config and the provisioned infrastructure (read-only): service accounts, IAM roles, gitlab tokens, github secrets, store entries.",
3372
+ group: "project",
3373
+ inputs: {
3374
+ component: {
3375
+ type: "string",
3376
+ message: "component name",
3377
+ positional: true,
3378
+ required: false
3379
+ }
3380
+ },
3381
+ execute: async ctx => {
3382
+ const component = await ctx.get("component");
3383
+ await (0, doctor_1.doctorProject)(ctx, component);
3384
+ }
3385
+ });
3386
+
3387
+ /***/ }),
3388
+
2753
3389
  /***/ 47838:
2754
3390
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2755
3391
 
@@ -4743,11 +5379,12 @@ exports.MissingInputError = MissingInputError;
4743
5379
  Object.defineProperty(exports, "__esModule", ({
4744
5380
  value: true
4745
5381
  }));
4746
- exports.upsertGcloudArtifactsRegistry = void 0;
5382
+ exports.upsertGcloudArtifactsRegistry = exports.CATLADDER_REGISTRY_NAME = void 0;
4747
5383
  const child_process_promise_1 = __nccwpck_require__(51748);
5384
+ exports.CATLADDER_REGISTRY_NAME = "catladder-deploy";
4748
5385
  const upsertGcloudArtifactsRegistry = async config => {
4749
5386
  try {
4750
- await (0, child_process_promise_1.exec)(`gcloud artifacts repositories create catladder-deploy --project="${config.projectId}" --repository-format=docker --location=${config.region}`);
5387
+ await (0, child_process_promise_1.exec)(`gcloud artifacts repositories create ${exports.CATLADDER_REGISTRY_NAME} --project="${config.projectId}" --repository-format=docker --location=${config.region}`);
4751
5388
  } catch (e) {
4752
5389
  // probably already exists
4753
5390
  //
@@ -5006,7 +5643,7 @@ exports.openGoogleCloudDashboard = openGoogleCloudDashboard;
5006
5643
 
5007
5644
  /***/ }),
5008
5645
 
5009
- /***/ 35611:
5646
+ /***/ 83798:
5010
5647
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
5011
5648
 
5012
5649
  "use strict";
@@ -5015,26 +5652,21 @@ exports.openGoogleCloudDashboard = openGoogleCloudDashboard;
5015
5652
  Object.defineProperty(exports, "__esModule", ({
5016
5653
  value: true
5017
5654
  }));
5018
- exports.upsertGcloudServiceAccountAndSaveSecret = exports.accountExists = void 0;
5019
- const child_process_promise_1 = __nccwpck_require__(51748);
5655
+ exports.getGcloudServiceAccountNames = void 0;
5020
5656
  const crypto_1 = __nccwpck_require__(76982);
5021
- const gitlab_1 = __nccwpck_require__(4508);
5022
- const accountExists = async fullIdentifier => {
5023
- try {
5024
- await (0, child_process_promise_1.exec)(`gcloud iam service-accounts describe ${fullIdentifier}`);
5025
- return true;
5026
- } catch (_a) {
5027
- return false;
5028
- }
5029
- };
5030
- exports.accountExists = accountExists;
5031
- const upsertGcloudServiceAccount = async (context, account) => {
5657
+ /**
5658
+ * the deterministic gcloud service account naming used by
5659
+ * `project setup`. Extracted so `project doctor` can compute the
5660
+ * expected identifier of an account without creating anything.
5661
+ *
5662
+ * gcloud limits account ids to 30 chars: the customer/app middle part
5663
+ * (and, if even that is not enough, the env/component suffix) is
5664
+ * replaced by a hash when the full name would not fit.
5665
+ */
5666
+ const getGcloudServiceAccountNames = (context, account) => {
5032
5667
  const {
5033
5668
  projectId,
5034
- name,
5035
- displayName,
5036
- roles,
5037
- description
5669
+ name
5038
5670
  } = account;
5039
5671
  // name has limit of 30
5040
5672
  const namePrefix = `${name}`;
@@ -5055,8 +5687,54 @@ const upsertGcloudServiceAccount = async (context, account) => {
5055
5687
  nameSuffix = nameSuffixRaw;
5056
5688
  }
5057
5689
  const fullName = `${namePrefix}-${nameMiddle}-${nameSuffix}`;
5058
- const fullDisplayName = `${context.fullConfig.customerName}-${context.fullConfig.appName} ${context.env}:${context.name} | ${displayName}`;
5059
5690
  const fullIdentifier = `${fullName}@${projectId}.iam.gserviceaccount.com`;
5691
+ return {
5692
+ fullName,
5693
+ fullIdentifier
5694
+ };
5695
+ };
5696
+ exports.getGcloudServiceAccountNames = getGcloudServiceAccountNames;
5697
+ const hashIfNessecary = (str, maxLength) => str.length > maxLength ? hash(str, maxLength) : str;
5698
+ const hash = (str, length) => {
5699
+ return (0, crypto_1.createHmac)("sha256", str).digest("hex").substring(0, length);
5700
+ };
5701
+
5702
+ /***/ }),
5703
+
5704
+ /***/ 35611:
5705
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
5706
+
5707
+ "use strict";
5708
+
5709
+
5710
+ Object.defineProperty(exports, "__esModule", ({
5711
+ value: true
5712
+ }));
5713
+ exports.upsertGcloudServiceAccountAndSaveSecret = exports.accountExists = void 0;
5714
+ const child_process_promise_1 = __nccwpck_require__(51748);
5715
+ const gitlab_1 = __nccwpck_require__(4508);
5716
+ const serviceAccountNames_1 = __nccwpck_require__(83798);
5717
+ const accountExists = async fullIdentifier => {
5718
+ try {
5719
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts describe ${fullIdentifier}`);
5720
+ return true;
5721
+ } catch (_a) {
5722
+ return false;
5723
+ }
5724
+ };
5725
+ exports.accountExists = accountExists;
5726
+ const upsertGcloudServiceAccount = async (context, account) => {
5727
+ const {
5728
+ projectId,
5729
+ displayName,
5730
+ roles,
5731
+ description
5732
+ } = account;
5733
+ const {
5734
+ fullName,
5735
+ fullIdentifier
5736
+ } = (0, serviceAccountNames_1.getGcloudServiceAccountNames)(context, account);
5737
+ const fullDisplayName = `${context.fullConfig.customerName}-${context.fullConfig.appName} ${context.env}:${context.name} | ${displayName}`;
5060
5738
  const existing = await (0, exports.accountExists)(fullIdentifier);
5061
5739
  if (!existing) {
5062
5740
  await (0, child_process_promise_1.exec)(`gcloud iam service-accounts create ${fullName} --display-name="${fullDisplayName}" --project="${projectId}" --description="${description}"`);
@@ -5084,10 +5762,6 @@ const upsertGcloudServiceAccountAndSaveSecret = async (instance, context, accoun
5084
5762
  instance.log("done!");
5085
5763
  };
5086
5764
  exports.upsertGcloudServiceAccountAndSaveSecret = upsertGcloudServiceAccountAndSaveSecret;
5087
- const hashIfNessecary = (str, maxLength) => str.length > maxLength ? hash(str, maxLength) : str;
5088
- const hash = (str, length) => {
5089
- return (0, crypto_1.createHmac)("sha256", str).digest("hex").substring(0, length);
5090
- };
5091
5765
 
5092
5766
  /***/ }),
5093
5767
 
@@ -5665,6 +6339,40 @@ exports.connectToCluster = connectToCluster;
5665
6339
 
5666
6340
  /***/ }),
5667
6341
 
6342
+ /***/ 5092:
6343
+ /***/ ((__unused_webpack_module, exports) => {
6344
+
6345
+ "use strict";
6346
+
6347
+
6348
+ Object.defineProperty(exports, "__esModule", ({
6349
+ value: true
6350
+ }));
6351
+ exports.mapWithConcurrency = void 0;
6352
+ /**
6353
+ * maps over items with at most `limit` calls in flight, preserving the
6354
+ * input order in the result (unlike reporting-as-you-go, this lets
6355
+ * callers run slow per-item lookups in parallel and still log the
6356
+ * outcomes in a stable order)
6357
+ */
6358
+ const mapWithConcurrency = async (items, limit, fn) => {
6359
+ const results = new Array(items.length);
6360
+ let next = 0;
6361
+ const workers = Array.from({
6362
+ length: Math.max(1, Math.min(limit, items.length))
6363
+ }, async () => {
6364
+ while (next < items.length) {
6365
+ const index = next++;
6366
+ results[index] = await fn(items[index], index);
6367
+ }
6368
+ });
6369
+ await Promise.all(workers);
6370
+ return results;
6371
+ };
6372
+ exports.mapWithConcurrency = mapWithConcurrency;
6373
+
6374
+ /***/ }),
6375
+
5668
6376
  /***/ 82805:
5669
6377
  /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
5670
6378
 
@@ -5778,7 +6486,7 @@ exports["default"] = async () => {
5778
6486
  Object.defineProperty(exports, "__esModule", ({
5779
6487
  value: true
5780
6488
  }));
5781
- exports.ensureGithubEnvironment = exports.setGithubVariable = exports.setGithubSecret = exports.getGithubRepoFromRemote = exports.isGhAuthenticated = void 0;
6489
+ exports.ensureGithubEnvironment = exports.setGithubVariable = exports.setGithubSecret = exports.listGithubVariableNames = exports.listGithubSecretNames = exports.listGithubEnvironments = exports.getGithubRepoFromRemote = exports.isGhAuthenticated = void 0;
5782
6490
  const child_process_1 = __nccwpck_require__(35317);
5783
6491
  const util_1 = __nccwpck_require__(39023);
5784
6492
  const execFile = (0, util_1.promisify)(child_process_1.execFile);
@@ -5811,6 +6519,31 @@ const getGithubRepoFromRemote = async remoteName => {
5811
6519
  }
5812
6520
  };
5813
6521
  exports.getGithubRepoFromRemote = getGithubRepoFromRemote;
6522
+ /**
6523
+ * runs a paginated `gh api` query and returns the jq-extracted lines
6524
+ * (one value per line, e.g. names)
6525
+ */
6526
+ const ghApiLines = async (path, jq) => {
6527
+ const {
6528
+ stdout
6529
+ } = await execFile("gh", ["api", "--paginate", path, "--jq", jq], {
6530
+ maxBuffer: 10 * 1024 * 1024
6531
+ });
6532
+ return stdout.split("\n").map(line => line.trim()).filter(Boolean);
6533
+ };
6534
+ /** names of all deployment environments of the repository */
6535
+ const listGithubEnvironments = repo => ghApiLines(`repos/${repo}/environments?per_page=100`, ".environments[].name");
6536
+ exports.listGithubEnvironments = listGithubEnvironments;
6537
+ /**
6538
+ * names of the actions secrets — of an environment, or of the repo
6539
+ * level when no environment is given. Github secrets are write-only:
6540
+ * names are all that can ever be inspected.
6541
+ */
6542
+ const listGithubSecretNames = (repo, environment) => ghApiLines(environment ? `repos/${repo}/environments/${encodeURIComponent(environment)}/secrets?per_page=100` : `repos/${repo}/actions/secrets?per_page=100`, ".secrets[].name");
6543
+ exports.listGithubSecretNames = listGithubSecretNames;
6544
+ /** names of the actions variables (environment or repo level) */
6545
+ const listGithubVariableNames = (repo, environment) => ghApiLines(environment ? `repos/${repo}/environments/${encodeURIComponent(environment)}/variables?per_page=100` : `repos/${repo}/actions/variables?per_page=100`, ".variables[].name");
6546
+ exports.listGithubVariableNames = listGithubVariableNames;
5814
6547
  const ghSetViaStdin = (kind, repo, name, value, environment) => new Promise((resolve, reject) => {
5815
6548
  var _a, _b;
5816
6549
  const child = (0, child_process_1.spawn)("gh", [kind, "set", name, "--repo", repo, ...(environment ? ["--env", environment] : [])], {
@@ -5866,7 +6599,7 @@ var __importDefault = this && this.__importDefault || function (mod) {
5866
6599
  Object.defineProperty(exports, "__esModule", ({
5867
6600
  value: true
5868
6601
  }));
5869
- exports.upsertAllVariables = exports.clearBackups = exports.getVariableValueByRawName = exports.getAllVariables = exports.getProjectInfo = exports.doGitlabRequest = exports.getGitlabToken = exports.setupGitlabToken = exports.hasGitlabToken = void 0;
6602
+ exports.upsertAllVariables = exports.clearBackups = exports.getVariableValueByRawName = exports.getAllVariables = exports.getProjectInfo = exports.doGitlabRequestAllPages = exports.doGitlabRequest = exports.getGitlabToken = exports.setupGitlabToken = exports.hasGitlabToken = void 0;
5870
6603
  const pipeline_1 = __nccwpck_require__(60241);
5871
6604
  const lodash_es_1 = __nccwpck_require__(64284);
5872
6605
  const memoizee_1 = __importDefault(__nccwpck_require__(57009));
@@ -5937,6 +6670,24 @@ const doGitlabRequest = async (io, path, data = undefined, method = "GET") => {
5937
6670
  throw new Error(`Could not send request to gitlab api ${path}: ${result.status} "${result.statusText}".\nResponse: ${JSON.stringify(await result.json(), null, 2)}`);
5938
6671
  };
5939
6672
  exports.doGitlabRequest = doGitlabRequest;
6673
+ /**
6674
+ * GET a list endpoint across ALL pages (gitlab returns 20 items per
6675
+ * page by default — long-lived projects easily exceed that, e.g.
6676
+ * rotated access tokens accumulate one entry per rotation)
6677
+ */
6678
+ const doGitlabRequestAllPages = async (io, path) => {
6679
+ const PER_PAGE = 100;
6680
+ const separator = path.includes("?") ? "&" : "?";
6681
+ const all = [];
6682
+ for (let page = 1;; page++) {
6683
+ const items = await (0, exports.doGitlabRequest)(io, `${path}${separator}per_page=${PER_PAGE}&page=${page}`);
6684
+ if (!(items === null || items === void 0 ? void 0 : items.length)) break;
6685
+ all.push(...items);
6686
+ if (items.length < PER_PAGE) break;
6687
+ }
6688
+ return all;
6689
+ };
6690
+ exports.doGitlabRequestAllPages = doGitlabRequestAllPages;
5940
6691
  const getProjectInfo = async io => {
5941
6692
  const {
5942
6693
  gitRemotePath
@@ -7205,13 +7956,25 @@ const resolveGithubNeeds = (context, job, allJobs, extraNeeds = []) => {
7205
7956
  return [...new Map(resolved.map(n => [n.id, n])).values()].sort((a, b) => a.id.localeCompare(b.id));
7206
7957
  };
7207
7958
  exports.resolveGithubNeeds = resolveGithubNeeds;
7959
+ /** step id of a referenceable cache restore step (see `cacheId`) */
7960
+ const cacheStepId = cacheId => `cache-${cacheId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
7208
7961
  const createCacheSteps = (context, job) => {
7209
7962
  var _a, _b;
7210
7963
  if (!((_a = job.caches) === null || _a === void 0 ? void 0 : _a.length)) {
7211
7964
  return [];
7212
7965
  }
7213
- const lowered = (_b = (0, createJobCache_1.createJobCacheFromCacheConfigs)(context, job.caches)) !== null && _b !== void 0 ? _b : [];
7214
- return (0, utils_1.ensureArray)(lowered).map(cache => {
7966
+ const lowered = (0, utils_1.ensureArray)((_b = (0, createJobCache_1.createJobCacheFromCacheConfigs)(context, job.caches)) !== null && _b !== void 0 ? _b : []);
7967
+ // caches marked redundantOnExactHitOf skip their (expensive) restore
7968
+ // when the referenced sibling cache hit its exact key — e.g. the yarn
7969
+ // zip cache is never read when node_modules restored for the same
7970
+ // lockfile. The referenced cache's step must come first (its
7971
+ // `cache-hit` output drives the `if`), so providers are emitted
7972
+ // before dependents; the reorder is github-only and does not affect
7973
+ // the gitlab cache list.
7974
+ const referencedIds = new Set(lowered.map(cache => cache.redundantOnExactHitOf).filter(id => !!id));
7975
+ const providedIds = new Set(lowered.map(cache => cache.cacheId).filter(id => !!id));
7976
+ const ordered = [...lowered.filter(cache => !cache.redundantOnExactHitOf), ...lowered.filter(cache => cache.redundantOnExactHitOf)];
7977
+ return ordered.map(cache => {
7215
7978
  var _a, _b, _c, _d, _e, _f, _g;
7216
7979
  const paths = (0, utils_1.ensureArray)((_a = cache.paths) !== null && _a !== void 0 ? _a : []);
7217
7980
  const key = cache.key;
@@ -7236,8 +7999,18 @@ const createCacheSteps = (context, job) => {
7236
7999
  restoreKeyPrefix: `${(_e = key.prefix) !== null && _e !== void 0 ? _e : "files"}-`,
7237
8000
  label: (_f = key.prefix) !== null && _f !== void 0 ? _f : ((_g = key.files) !== null && _g !== void 0 ? _g : []).join(",")
7238
8001
  };
8002
+ // the sibling-hit condition only applies when the referenced cache
8003
+ // is actually part of this job — otherwise restore unconditionally
8004
+ const skipCondition = cache.redundantOnExactHitOf && providedIds.has(cache.redundantOnExactHitOf) ? `\${{ steps.${cacheStepId(cache.redundantOnExactHitOf)}.outputs.cache-hit != 'true' }}` : undefined;
7239
8005
  return {
7240
8006
  name: `Cache ${label}`,
8007
+ // referenced caches carry an id so dependents can read cache-hit
8008
+ ...(cache.cacheId && referencedIds.has(cache.cacheId) ? {
8009
+ id: cacheStepId(cache.cacheId)
8010
+ } : {}),
8011
+ ...(skipCondition ? {
8012
+ if: skipCondition
8013
+ } : {}),
7241
8014
  // pull-only caches are restored but never saved
7242
8015
  uses: cache.policy === "pull" ? "actions/cache/restore@v4" : "actions/cache@v4",
7243
8016
  with: {
@@ -8012,15 +8785,16 @@ const makeGitlabJob = (context, job, allJobs, images, baseRules) => {
8012
8785
  const when = jobWhen !== null && jobWhen !== void 0 ? jobWhen : gate ? gate === "manual" ? "manual" : "on_success" : undefined;
8013
8786
  const allow_failure = jobAllowFailure !== null && jobAllowFailure !== void 0 ? jobAllowFailure : gate ? gate === "manual" : undefined;
8014
8787
  // the neutral `caches` declarations resolve to gitlab cache configs;
8015
- // an explicitly set gitlab `cache` takes precedence. `keyFiles` is a
8016
- // github-only content-key hint and must not leak into the gitlab yaml
8017
- const stripKeyFiles = resolved => Array.isArray(resolved) ? resolved.map(({
8788
+ // an explicitly set gitlab `cache` takes precedence. keyFiles /
8789
+ // cacheId / redundantOnExactHitOf are github-only hints (content
8790
+ // keys, conditional restore) and must not leak into the gitlab yaml
8791
+ const stripGithubCacheHints = ({
8018
8792
  keyFiles: _keyFiles,
8793
+ cacheId: _cacheId,
8794
+ redundantOnExactHitOf: _redundantOnExactHitOf,
8019
8795
  ...cacheRest
8020
- }) => cacheRest) : (({
8021
- keyFiles: _keyFiles,
8022
- ...cacheRest
8023
- }) => cacheRest)(resolved);
8796
+ }) => cacheRest;
8797
+ const stripKeyFiles = resolved => Array.isArray(resolved) ? resolved.map(stripGithubCacheHints) : stripGithubCacheHints(resolved);
8024
8798
  const resolvedCaches = caches && context.type !== "agent" ? (0, createJobCache_1.createJobCacheFromCacheConfigs)(context, caches) : undefined;
8025
8799
  const cache = jobCache !== null && jobCache !== void 0 ? jobCache : resolvedCaches && stripKeyFiles(resolvedCaches);
8026
8800
  const stage = envMode === "stagePerEnv" && context.type !== "agent" ? `${job.stage} ${context.env}` : job.stage;
@@ -9314,6 +10088,8 @@ const createJobCacheFromCacheConfigs = (context, caches) => {
9314
10088
  pathMode,
9315
10089
  buildDir,
9316
10090
  keyFiles,
10091
+ cacheId,
10092
+ redundantOnExactHitOf,
9317
10093
  ...rest
9318
10094
  }) => {
9319
10095
  var _a;
@@ -9330,6 +10106,12 @@ const createJobCacheFromCacheConfigs = (context, caches) => {
9330
10106
  ...((keyFiles === null || keyFiles === void 0 ? void 0 : keyFiles.length) ? {
9331
10107
  keyFiles: keyFiles.map(f => pathMode === "absolute" ? f : (0, path_1.join)(baseDir, f))
9332
10108
  } : {}),
10109
+ ...(cacheId ? {
10110
+ cacheId
10111
+ } : {}),
10112
+ ...(redundantOnExactHitOf ? {
10113
+ redundantOnExactHitOf
10114
+ } : {}),
9333
10115
  ...rest
9334
10116
  };
9335
10117
  });
@@ -9873,6 +10655,12 @@ exports.getNodeCache = exports.getNodeModulesCache = exports.getYarnCache = void
9873
10655
  const lodash_es_1 = __nccwpck_require__(64284);
9874
10656
  const path_1 = __nccwpck_require__(16928);
9875
10657
  const slugify_1 = __importDefault(__nccwpck_require__(54206));
10658
+ /**
10659
+ * stable reference between the two node caches: the yarn (zip) cache
10660
+ * declares itself redundant when the node_modules cache scored an
10661
+ * exact-lockfile hit (github skips the ~0.6GB download on warm runs)
10662
+ */
10663
+ const NODE_MODULES_CACHE_ID = "node-modules";
9876
10664
  const getYarnCache = async (context, policy = "pull-push") => {
9877
10665
  const packageManagerInfo = await context.packageManagerInfo;
9878
10666
  const componentIsInWorkspace = context.type === "component" && "componentIsInWorkspace" in packageManagerInfo && packageManagerInfo.componentIsInWorkspace;
@@ -9884,7 +10672,12 @@ const getYarnCache = async (context, policy = "pull-push") => {
9884
10672
  paths: [".yarn"],
9885
10673
  // content-key for immutable-cache backends (github); the lockfile
9886
10674
  // decides whether the cached content could have changed
9887
- keyFiles: ["yarn.lock"]
10675
+ keyFiles: ["yarn.lock"],
10676
+ // the .yarn zips (~hundreds of MB) are only read when yarn has to
10677
+ // (re)install packages. When the node_modules cache hit for the
10678
+ // same lockfile, install verifies via install-state.gz and never
10679
+ // opens a zip — so github skips downloading this cache entirely
10680
+ redundantOnExactHitOf: NODE_MODULES_CACHE_ID
9888
10681
  }];
9889
10682
  };
9890
10683
  exports.getYarnCache = getYarnCache;
@@ -9908,6 +10701,9 @@ const getNodeModulesCache = async (context, policy = "pull-push") => {
9908
10701
  // decides whether the cached content could have changed (absolute
9909
10702
  // pathMode: resolve the standalone component's lockfile explicitly)
9910
10703
  keyFiles: [componentIsInWorkspace ? "yarn.lock" : (0, path_1.join)(context.build.dir, "yarn.lock")],
10704
+ // referenced by the yarn cache: an exact hit here makes the yarn
10705
+ // zips unnecessary (see redundantOnExactHitOf on getYarnCache)
10706
+ cacheId: NODE_MODULES_CACHE_ID,
9911
10707
  policy,
9912
10708
  paths: [...(componentIsInWorkspace ? (0, lodash_es_1.uniq)(["node_modules", ...((_a = workspaces.map(w => (0, path_1.join)(w.location, "node_modules"))) !== null && _a !== void 0 ? _a : []), ...(!isClassic ? [".yarn/install-state.gz", ...((_b = workspaces.map(w => (0, path_1.join)(w.location, ".yarn/install-state.gz"))) !== null && _b !== void 0 ? _b : [])] : [])]) : [(0, path_1.join)(context.build.dir, "node_modules"), ...(!isClassic ? [(0, path_1.join)(context.build.dir, ".yarn/install-state.gz")] : [])])]
9913
10709
  }];
@@ -10595,7 +11391,7 @@ Object.defineProperty(exports, "__esModule", ({
10595
11391
  value: true
10596
11392
  }));
10597
11393
  exports.DOCKER_REGISTRY = exports.PIPELINE_IMAGE_TAG = void 0;
10598
- exports.PIPELINE_IMAGE_TAG = "next-71c49b28" || 0;
11394
+ exports.PIPELINE_IMAGE_TAG = "next-5dc1f907" || 0;
10599
11395
  exports.DOCKER_REGISTRY = "git.panter.ch:5001/catladder/catladder" || 0;
10600
11396
 
10601
11397
  /***/ }),