@ubiquity-os/plugin-sdk 3.6.3 → 3.7.0

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.
@@ -2707,14 +2707,101 @@ var manifestSchema = T2.Object({
2707
2707
  var CONFIG_PROD_FULL_PATH = ".github/.ubiquity-os.config.yml";
2708
2708
  var CONFIG_DEV_FULL_PATH = ".github/.ubiquity-os.config.dev.yml";
2709
2709
  var CONFIG_ORG_REPO = ".ubiquity-os";
2710
+ var ENVIRONMENT_TO_CONFIG_SUFFIX = {
2711
+ development: "dev"
2712
+ };
2713
+ var VALID_CONFIG_SUFFIX = /^[a-z0-9][a-z0-9_-]*$/i;
2714
+ var MAX_IMPORT_DEPTH = 6;
2715
+ function normalizeEnvironmentName(environment) {
2716
+ return String(environment ?? "").trim().toLowerCase();
2717
+ }
2718
+ function getConfigPathCandidatesForEnvironment(environment) {
2719
+ const normalized = normalizeEnvironmentName(environment);
2720
+ if (!normalized) {
2721
+ return [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
2722
+ }
2723
+ if (normalized === "production" || normalized === "prod") {
2724
+ return [CONFIG_PROD_FULL_PATH];
2725
+ }
2726
+ const suffix = ENVIRONMENT_TO_CONFIG_SUFFIX[normalized] ?? normalized;
2727
+ if (suffix === "dev") {
2728
+ return [CONFIG_DEV_FULL_PATH];
2729
+ }
2730
+ if (!VALID_CONFIG_SUFFIX.test(suffix)) {
2731
+ return [CONFIG_DEV_FULL_PATH];
2732
+ }
2733
+ return [`.github/.ubiquity-os.config.${suffix}.yml`, CONFIG_PROD_FULL_PATH];
2734
+ }
2735
+ function normalizeImportKey(location) {
2736
+ return `${location.owner}`.trim().toLowerCase() + "/" + `${location.repo}`.trim().toLowerCase();
2737
+ }
2738
+ function parseImportSpec(value) {
2739
+ const trimmed = value.trim();
2740
+ if (!trimmed) return null;
2741
+ const parts = trimmed.split("/");
2742
+ if (parts.length !== 2) return null;
2743
+ const [owner, repo] = parts;
2744
+ if (!owner || !repo) return null;
2745
+ return { owner, repo };
2746
+ }
2747
+ function readImports(logger, value, source) {
2748
+ if (!value) return [];
2749
+ if (!Array.isArray(value)) {
2750
+ logger.warn("Invalid imports; expected a list of strings.", { source });
2751
+ return [];
2752
+ }
2753
+ const seen = /* @__PURE__ */ new Set();
2754
+ const imports = [];
2755
+ for (const entry of value) {
2756
+ if (typeof entry !== "string") {
2757
+ logger.warn("Ignoring invalid import entry; expected string.", { source, entry });
2758
+ continue;
2759
+ }
2760
+ const parsed = parseImportSpec(entry);
2761
+ if (!parsed) {
2762
+ logger.warn("Ignoring invalid import entry; expected owner/repo.", { source, entry });
2763
+ continue;
2764
+ }
2765
+ const key = normalizeImportKey(parsed);
2766
+ if (seen.has(key)) continue;
2767
+ seen.add(key);
2768
+ imports.push(parsed);
2769
+ }
2770
+ return imports;
2771
+ }
2772
+ function stripImports(config) {
2773
+ if (!config || typeof config !== "object") return config;
2774
+ const { imports: _imports, ...rest } = config;
2775
+ return rest;
2776
+ }
2777
+ function mergeImportedConfigs(imported, base) {
2778
+ if (!imported.length) {
2779
+ return base;
2780
+ }
2781
+ let merged = imported[0];
2782
+ for (let i = 1; i < imported.length; i++) {
2783
+ merged = {
2784
+ ...merged,
2785
+ ...imported[i],
2786
+ plugins: { ...merged.plugins, ...imported[i].plugins }
2787
+ };
2788
+ }
2789
+ return base ? {
2790
+ ...merged,
2791
+ ...base,
2792
+ plugins: { ...merged.plugins, ...base.plugins }
2793
+ } : merged;
2794
+ }
2710
2795
  var ConfigurationHandler = class {
2711
- constructor(_logger, _octokit, _environment = null) {
2796
+ constructor(_logger, _octokit, _environment = null, options) {
2712
2797
  this._logger = _logger;
2713
2798
  this._octokit = _octokit;
2714
2799
  this._environment = _environment;
2800
+ this._octokitFactory = options?.octokitFactory;
2715
2801
  }
2716
2802
  _manifestCache = {};
2717
2803
  _manifestPromiseCache = {};
2804
+ _octokitFactory;
2718
2805
  /**
2719
2806
  * Retrieves the configuration for the current plugin based on its manifest.
2720
2807
  * @param manifest - The plugin manifest containing the `short_name` identifier
@@ -2733,7 +2820,7 @@ var ConfigurationHandler = class {
2733
2820
  * @returns The merged plugin configuration with resolved plugin settings.
2734
2821
  */
2735
2822
  async getConfiguration(location) {
2736
- const defaultConfiguration = Value.Decode(configSchema, Value.Default(configSchema, {}));
2823
+ const defaultConfiguration = stripImports(Value.Decode(configSchema, Value.Default(configSchema, {})));
2737
2824
  if (!location) {
2738
2825
  this._logger.info("No location was provided, using the default configuration");
2739
2826
  return defaultConfiguration;
@@ -2794,57 +2881,153 @@ var ConfigurationHandler = class {
2794
2881
  * @param repository The repository name
2795
2882
  */
2796
2883
  async getConfigurationFromRepo(owner, repository) {
2884
+ const location = { owner, repo: repository };
2885
+ const state = this._createImportState();
2886
+ const octokit = await this._getOctokitForLocation(location, state);
2887
+ if (!octokit) {
2888
+ this._logger.warn("No Octokit available for configuration load", { owner, repository });
2889
+ return { config: null, errors: null, rawData: null };
2890
+ }
2891
+ const { config, imports, errors, rawData } = await this._loadConfigSource(location, octokit);
2892
+ if (!rawData) {
2893
+ return { config: null, errors: null, rawData: null };
2894
+ }
2895
+ if (errors && errors.length) {
2896
+ this._logger.warn("YAML could not be decoded", { owner, repository, errors });
2897
+ return { config: null, errors, rawData };
2898
+ }
2899
+ if (!config) {
2900
+ this._logger.warn("YAML could not be decoded", { owner, repository });
2901
+ return { config: null, errors, rawData };
2902
+ }
2903
+ const importedConfigs = [];
2904
+ for (const next of imports) {
2905
+ const resolved = await this._resolveImportedConfiguration(next, state, 1);
2906
+ if (resolved) importedConfigs.push(resolved);
2907
+ }
2908
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
2909
+ if (!mergedConfig) {
2910
+ return { config: null, errors: null, rawData };
2911
+ }
2912
+ const decoded = this._decodeConfiguration(location, mergedConfig);
2913
+ return { config: decoded.config, errors: decoded.errors, rawData };
2914
+ }
2915
+ _createImportState() {
2916
+ return {
2917
+ cache: /* @__PURE__ */ new Map(),
2918
+ inFlight: /* @__PURE__ */ new Set(),
2919
+ octokitByLocation: /* @__PURE__ */ new Map()
2920
+ };
2921
+ }
2922
+ async _getOctokitForLocation(location, state) {
2923
+ const key = normalizeImportKey(location);
2924
+ if (state.octokitByLocation.has(key)) {
2925
+ return state.octokitByLocation.get(key) ?? null;
2926
+ }
2927
+ if (this._octokitFactory) {
2928
+ const resolved = await this._octokitFactory(location);
2929
+ if (resolved) {
2930
+ state.octokitByLocation.set(key, resolved);
2931
+ return resolved;
2932
+ }
2933
+ }
2934
+ state.octokitByLocation.set(key, this._octokit);
2935
+ return this._octokit;
2936
+ }
2937
+ async _loadConfigSource(location, octokit) {
2797
2938
  const rawData = await this._download({
2798
- repository,
2799
- owner
2939
+ repository: location.repo,
2940
+ owner: location.owner,
2941
+ octokit
2800
2942
  });
2801
- this._logger.ok("Downloaded configuration file", { owner, repository });
2943
+ this._logger.ok("Downloaded configuration file", { owner: location.owner, repository: location.repo });
2802
2944
  if (!rawData) {
2803
- this._logger.warn("No raw configuration data", { owner, repository });
2804
- return { config: null, errors: null, rawData: null };
2945
+ this._logger.warn("No raw configuration data", { owner: location.owner, repository: location.repo });
2946
+ return { config: null, imports: [], errors: null, rawData: null };
2805
2947
  }
2806
2948
  const { yaml, errors } = this._parseYaml(rawData);
2949
+ const imports = readImports(this._logger, yaml?.imports, location);
2950
+ if (yaml && typeof yaml === "object" && !Array.isArray(yaml) && "imports" in yaml) {
2951
+ delete yaml.imports;
2952
+ }
2807
2953
  const targetRepoConfiguration = yaml;
2808
- this._logger.info("Decoding configuration", { owner, repository });
2809
- if (targetRepoConfiguration) {
2810
- try {
2811
- const configSchemaWithDefaults = Value.Default(configSchema, targetRepoConfiguration);
2812
- const errors2 = Value.Errors(configSchema, configSchemaWithDefaults);
2813
- if (errors2.First()) {
2814
- for (const error of errors2) {
2815
- this._logger.warn("Configuration validation error", { err: error });
2816
- }
2954
+ return { config: targetRepoConfiguration, imports, errors, rawData };
2955
+ }
2956
+ _decodeConfiguration(location, config) {
2957
+ this._logger.info("Decoding configuration", { owner: location.owner, repository: location.repo });
2958
+ try {
2959
+ const configSchemaWithDefaults = Value.Default(configSchema, config);
2960
+ const errors = Value.Errors(configSchema, configSchemaWithDefaults);
2961
+ if (errors.First()) {
2962
+ for (const error of errors) {
2963
+ this._logger.warn("Configuration validation error", { err: error });
2817
2964
  }
2818
- const decodedConfig = Value.Decode(configSchema, configSchemaWithDefaults);
2819
- return { config: decodedConfig, errors: errors2.First() ? errors2 : null, rawData };
2820
- } catch (error) {
2821
- this._logger.warn("Error decoding configuration; Will ignore.", { err: error, owner, repository });
2822
- return { config: null, errors: [error instanceof TransformDecodeCheckError ? error.error : error], rawData };
2823
2965
  }
2966
+ const decodedConfig = Value.Decode(configSchema, configSchemaWithDefaults);
2967
+ return { config: stripImports(decodedConfig), errors: errors.First() ? errors : null };
2968
+ } catch (error) {
2969
+ this._logger.warn("Error decoding configuration; Will ignore.", { err: error, owner: location.owner, repository: location.repo });
2970
+ return { config: null, errors: [error instanceof TransformDecodeCheckError ? error.error : error] };
2824
2971
  }
2825
- this._logger.warn("YAML could not be decoded", { owner, repository, errors });
2826
- return { config: null, errors, rawData };
2827
2972
  }
2828
- async _download({ repository, owner }) {
2973
+ async _resolveImportedConfiguration(location, state, depth) {
2974
+ const key = normalizeImportKey(location);
2975
+ if (state.cache.has(key)) {
2976
+ return state.cache.get(key) ?? null;
2977
+ }
2978
+ if (state.inFlight.has(key)) {
2979
+ this._logger.warn("Skipping import due to circular reference.", { location });
2980
+ return null;
2981
+ }
2982
+ if (depth > MAX_IMPORT_DEPTH) {
2983
+ this._logger.warn("Skipping import; maximum depth exceeded.", { location, depth });
2984
+ return null;
2985
+ }
2986
+ state.inFlight.add(key);
2987
+ let resolved = null;
2988
+ try {
2989
+ const octokit = await this._getOctokitForLocation(location, state);
2990
+ if (!octokit) {
2991
+ this._logger.warn("Skipping import; no authorized Octokit for owner.", { location });
2992
+ return null;
2993
+ }
2994
+ const { config, imports, errors } = await this._loadConfigSource(location, octokit);
2995
+ if (errors && errors.length) {
2996
+ this._logger.warn("Skipping import due to YAML parsing errors.", { location, errors });
2997
+ return null;
2998
+ }
2999
+ if (!config) {
3000
+ return null;
3001
+ }
3002
+ const importedConfigs = [];
3003
+ for (const next of imports) {
3004
+ const nested = await this._resolveImportedConfiguration(next, state, depth + 1);
3005
+ if (nested) importedConfigs.push(nested);
3006
+ }
3007
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
3008
+ if (!mergedConfig) return null;
3009
+ const decoded = this._decodeConfiguration(location, mergedConfig);
3010
+ resolved = decoded.config;
3011
+ } finally {
3012
+ state.inFlight.delete(key);
3013
+ state.cache.set(key, resolved);
3014
+ }
3015
+ return resolved;
3016
+ }
3017
+ async _download({
3018
+ repository,
3019
+ owner,
3020
+ octokit
3021
+ }) {
2829
3022
  if (!repository || !owner) {
2830
3023
  this._logger.warn("Repo or owner is not defined, cannot download the requested file");
2831
3024
  return null;
2832
3025
  }
2833
- let pathList;
2834
- switch (this._environment) {
2835
- case "development":
2836
- pathList = [CONFIG_DEV_FULL_PATH];
2837
- break;
2838
- case "production":
2839
- pathList = [CONFIG_PROD_FULL_PATH];
2840
- break;
2841
- default:
2842
- pathList = [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
2843
- }
3026
+ const pathList = getConfigPathCandidatesForEnvironment(this._environment);
2844
3027
  for (const filePath of pathList) {
2845
3028
  try {
2846
3029
  this._logger.info("Attempting to fetch configuration", { owner, repository, filePath });
2847
- const { data, headers } = await this._octokit.rest.repos.getContent({
3030
+ const { data, headers } = await octokit.rest.repos.getContent({
2848
3031
  owner,
2849
3032
  repo: repository,
2850
3033
  path: filePath,
@@ -45,6 +45,7 @@ declare class CommentHandler {
45
45
  _extractIssueContext(context: Context): IssueContext | null;
46
46
  _processMessage(context: Context, message: LogReturn | Error): {
47
47
  metadata: {
48
+ status?: number | undefined;
48
49
  message: string;
49
50
  name: string;
50
51
  stack: string | undefined;
@@ -45,6 +45,7 @@ declare class CommentHandler {
45
45
  _extractIssueContext(context: Context): IssueContext | null;
46
46
  _processMessage(context: Context, message: LogReturn | Error): {
47
47
  metadata: {
48
+ status?: number | undefined;
48
49
  message: string;
49
50
  name: string;
50
51
  stack: string | undefined;
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EmitterWebhookEventName } from '@octokit/webhooks';
2
- import { C as Context } from './context-sqbr2o6i.mjs';
3
- export { a as CommentHandler } from './context-sqbr2o6i.mjs';
2
+ import { C as Context } from './context-Ckj1HMjz.mjs';
3
+ export { a as CommentHandler } from './context-Ckj1HMjz.mjs';
4
4
  import { TSchema, TAnySchema } from '@sinclair/typebox';
5
5
  import { LogLevel } from '@ubiquity-os/ubiquity-os-logger';
6
6
  import * as hono_types from 'hono/types';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EmitterWebhookEventName } from '@octokit/webhooks';
2
- import { C as Context } from './context-BbEmsEct.js';
3
- export { a as CommentHandler } from './context-BbEmsEct.js';
2
+ import { C as Context } from './context-BE4WjJZf.js';
3
+ export { a as CommentHandler } from './context-BE4WjJZf.js';
4
4
  import { TSchema, TAnySchema } from '@sinclair/typebox';
5
5
  import { LogLevel } from '@ubiquity-os/ubiquity-os-logger';
6
6
  import * as hono_types from 'hono/types';