@checkstack/gitops-common 0.2.1 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,138 @@
1
1
  # @checkstack/gitops-common
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f6f9a5c: Surface the source repository for GitOps-managed entities and gate the
8
+ system→group remove button on the system's lock state.
9
+
10
+ - `provenanceSchema` now carries a `sourceUrl` field, derived on the
11
+ backend from the provider type, baseUrl, repository and filePath. URLs
12
+ are constructed for github.com / gitlab.com and self-hosted
13
+ GitHub/GitLab where the API base ends in `/api/v3` or `/api/v4`. Other
14
+ baseUrls fall back to `null` so the UI keeps showing the raw path.
15
+ - New `useProvenanceLocks` hook (bulk variant of `useProvenanceLock`)
16
+ for views that render many entities and need to look up locks
17
+ client-side.
18
+ - New `<GitOpsSourceBadge>` popover component that replaces the bare
19
+ GitBranch icon on system and group catalog cards. The popover
20
+ surfaces the repository, file path, and a "View in source provider"
21
+ deep link.
22
+ - `<GitOpsLockBanner>` repo line is now a real link when a sourceUrl is
23
+ available.
24
+ - The system→group remove button in the catalog now disables itself
25
+ when the system is GitOps-managed, matching the backend lock that was
26
+ already in place.
27
+
28
+ ### Patch Changes
29
+
30
+ - Updated dependencies [42abfff]
31
+ - @checkstack/common@0.9.0
32
+
33
+ ## 0.2.2
34
+
35
+ ### Patch Changes
36
+
37
+ - 50e5f5f: Runtime plugin system: install + uninstall plugins from npm, GitHub releases
38
+ (including private GitHub Enterprise instances), or tarball uploads at
39
+ runtime, with multi-package bundles, dependency-derived compatibility checks,
40
+ multi-instance coordination via a Postgres artifact store, and
41
+ single-coordinator destructive cleanup.
42
+
43
+ Highlights:
44
+
45
+ - New `PluginSource` discriminated union and `PluginInstaller` /
46
+ `PluginInstallerRegistry` interfaces in `@checkstack/backend-api`. The
47
+ GitHub variant accepts an optional `apiBaseUrl` so deployments backed by
48
+ GitHub Enterprise can install from `https://ghe.example.com/api/v3`
49
+ instead of `api.github.com`.
50
+ - New `installPackageMetadataSchema` (Zod) in `@checkstack/common` validates
51
+ every plugin's `package.json` at install time. Required fields: `name`,
52
+ `version`, `description`, `author`, `license`, `checkstack.type`,
53
+ `checkstack.pluginId`. Optional: `checkstack.bundle`,
54
+ `checkstack.usageInstructions`, `checkstack.allowInstallScripts`.
55
+ - New `pluginManagerContract` in `@checkstack/pluginmanager-common` with
56
+ `list`, `previewInstall`, `install`, `previewUninstall`, `uninstall`, and
57
+ `events` procedures.
58
+ - New `@checkstack/pluginmanager-frontend` admin UI: installed-plugins list
59
+ with per-row uninstall (typed-confirmation modal, schema/configs/cascade
60
+ toggles), install page with NPM / Tarball Upload / GitHub Release tabs
61
+ (Catalog tab disabled — coming soon), and an events page surfacing the
62
+ install/uninstall audit log.
63
+ - New `bunx @checkstack/scripts plugin-pack` CLI for plugin authors —
64
+ per-package mode produces an npm-shaped tarball; `--bundle` mode produces
65
+ an outer tarball containing every sibling declared in
66
+ `package.json#checkstack.bundle`. Published to npm so external authors
67
+ can `bunx` it directly without a workspace checkout.
68
+ - Compatibility derived from `package.json#dependencies` ranges
69
+ (`semver.satisfies` against the platform's loaded `@checkstack/*`
70
+ versions) — no separate `compatibility` field.
71
+ - Multi-instance: originator persists artifacts + `plugins` rows + broadcasts
72
+ install/uninstall; receiving instances do in-process register/unregister
73
+ only. Destructive ops (drop schema, delete plugin_configs, delete
74
+ artifacts, delete `plugins` rows) run exactly once on the originator.
75
+ - Fresh-instance bootstrap: `loadPlugins()` hydrates any
76
+ `is_uninstallable=true` plugin missing from `node_modules` from the
77
+ artifact store before normal Phase 1 register.
78
+ - New schema: `plugin_artifacts` (tarball storage), `plugin_install_events`
79
+ (audit/error log). `plugins` extended with `version`, `metadata`,
80
+ `source`, `bundle_id`, `is_primary`. Local plugin sync now writes
81
+ `version` from each plugin's `package.json` so the admin UI shows real
82
+ versions instead of `—`.
83
+ - Tarball-upload endpoint (`POST /api/pluginmanager/upload-tarball`) for
84
+ the install UI; access-gated by `pluginmanager.plugin.manage`.
85
+ - Plugin Manager menu link added to the user menu (main grid, alongside
86
+ Profile / Notification Settings / etc.).
87
+
88
+ Cross-cutting changes:
89
+
90
+ - Backend request/response logging now flows through `rootLogger` (winston)
91
+ instead of `hono/logger`. 5xx responses include the response body inline
92
+ so swallowed early-return errors are visible in the log.
93
+ - The `/api/:pluginId/*` dispatcher now logs which core service is missing
94
+ or which `pluginId` had no metadata when it 500s.
95
+ - New `registerCorePluginMetadata` on `PluginManager` for core routers
96
+ (like the plugin manager itself) that need their metadata visible to the
97
+ RPC dispatcher without going through the full plugin lifecycle.
98
+ - ESLint: `unicorn/no-null` is now disabled globally. Drizzle distinguishes
99
+ between `null` (writes a real SQL NULL) and `undefined` (skip the column
100
+ on insert), so treating them as interchangeable produced latent bugs at
101
+ the persistence boundary. The bulk of the patch-bumped packages above
102
+ reflect lint-fix touches that landed when this rule was relaxed.
103
+ - Workspace-wide license normalization to `Elastic-2.0` (matches
104
+ `LICENSE.md`). Every `package.json` in the workspace now declares the
105
+ same SPDX identifier; the patch bumps capture this.
106
+
107
+ Plugin packages (every `plugins/*`): added a `pack` npm script
108
+ (`bunx @checkstack/scripts plugin-pack`), mirrored each plugin's
109
+ `pluginId` from `plugin-metadata.ts` into `package.json#checkstack.pluginId`
110
+ so install-time validation passes, stubbed any missing required metadata
111
+ fields (`description`, `author`, `license`), and added
112
+ `checkstack.bundle` to multi-package plugin primaries (telegram, rcon, ssh,
113
+ jira, queue-bullmq, queue-memory, cache-memory).
114
+
115
+ Breaking changes:
116
+
117
+ - The legacy single-method `PluginInstaller` interface (`install(packageName)`)
118
+ is removed. Callers must use `coreServices.pluginInstallerRegistry`.
119
+ - The old `pluginAdminContract` and `createPluginAdminRouter` are removed.
120
+ Replaced by `pluginManagerContract` in `@checkstack/pluginmanager-common`
121
+ and `createPluginManagerRouter` in `core/backend`.
122
+ - `@checkstack/test-utils-backend` no longer exports
123
+ `createMockPluginInstaller` / `MockPluginInstaller` (the legacy interface
124
+ it shimmed is gone).
125
+
126
+ Note: bumps are limited to `minor` (for packages with new public API
127
+ surface) and `patch` (for downstream consumers, license normalization,
128
+ and lint fixes). No `major` bumps despite the `PluginInstaller` removal —
129
+ the legacy interface had no third-party consumers in the wild before this
130
+ runtime plugin system landed, and the contract surface is the same shape
131
+ modulo the rename.
132
+
133
+ - Updated dependencies [50e5f5f]
134
+ - @checkstack/common@0.8.0
135
+
3
136
  ## 0.2.1
4
137
 
5
138
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@checkstack/gitops-common",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
+ "license": "Elastic-2.0",
4
5
  "type": "module",
5
6
  "exports": {
6
7
  ".": {
@@ -8,17 +9,17 @@
8
9
  }
9
10
  },
10
11
  "dependencies": {
11
- "@checkstack/common": "0.6.5",
12
+ "@checkstack/common": "0.8.0",
12
13
  "@orpc/contract": "^1.13.14",
13
14
  "zod": "^4.2.1"
14
15
  },
15
16
  "devDependencies": {
16
17
  "typescript": "^5.7.2",
17
- "@checkstack/tsconfig": "0.0.5",
18
- "@checkstack/scripts": "0.1.2"
18
+ "@checkstack/tsconfig": "0.0.7",
19
+ "@checkstack/scripts": "0.3.0"
19
20
  },
20
21
  "scripts": {
21
- "typecheck": "tsc --noEmit",
22
+ "typecheck": "tsgo -b",
22
23
  "lint": "bun run lint:code",
23
24
  "lint:code": "eslint . --max-warnings 0"
24
25
  },
package/src/index.ts CHANGED
@@ -36,3 +36,7 @@ export {
36
36
  type DeletionPolicy,
37
37
  } from "./provenance-types";
38
38
  export { gitopsContract, GitOpsApi, type GitOpsContract } from "./rpc-contract";
39
+ export {
40
+ deriveSourceUrl,
41
+ type DeriveSourceUrlInput,
42
+ } from "./source-url";
@@ -16,6 +16,12 @@ export const provenanceSchema = z.object({
16
16
  providerId: z.string(),
17
17
  repository: z.string(),
18
18
  filePath: z.string(),
19
+ /**
20
+ * Browsable web URL pointing to the entity's defining file in its source
21
+ * provider, derived from the provider type, baseUrl, repository and filePath.
22
+ * Null when the URL cannot be safely derived (unknown baseUrl shape, etc.).
23
+ */
24
+ sourceUrl: z.string().nullable(),
19
25
  lastSyncHash: z.string(),
20
26
  status: provenanceStatusSchema,
21
27
  errorMessage: z.string().nullable(),
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { deriveSourceUrl } from "./source-url";
3
+
4
+ describe("deriveSourceUrl", () => {
5
+ test("public github.com", () => {
6
+ expect(
7
+ deriveSourceUrl({
8
+ providerType: "github",
9
+ baseUrl: null,
10
+ repository: "acme/checkstack",
11
+ filePath: "catalog/system.yaml",
12
+ }),
13
+ ).toBe("https://github.com/acme/checkstack/blob/HEAD/catalog/system.yaml");
14
+ });
15
+
16
+ test("public gitlab.com", () => {
17
+ expect(
18
+ deriveSourceUrl({
19
+ providerType: "gitlab",
20
+ baseUrl: null,
21
+ repository: "acme/checkstack",
22
+ filePath: "catalog/system.yaml",
23
+ }),
24
+ ).toBe(
25
+ "https://gitlab.com/acme/checkstack/-/blob/HEAD/catalog/system.yaml",
26
+ );
27
+ });
28
+
29
+ test("api.github.com baseUrl maps to public host", () => {
30
+ expect(
31
+ deriveSourceUrl({
32
+ providerType: "github",
33
+ baseUrl: "https://api.github.com",
34
+ repository: "acme/checkstack",
35
+ filePath: "catalog/system.yaml",
36
+ }),
37
+ ).toBe("https://github.com/acme/checkstack/blob/HEAD/catalog/system.yaml");
38
+ });
39
+
40
+ test("github enterprise baseUrl strips /api/v3", () => {
41
+ expect(
42
+ deriveSourceUrl({
43
+ providerType: "github",
44
+ baseUrl: "https://github.example.com/api/v3",
45
+ repository: "acme/checkstack",
46
+ filePath: "catalog/system.yaml",
47
+ }),
48
+ ).toBe(
49
+ "https://github.example.com/acme/checkstack/blob/HEAD/catalog/system.yaml",
50
+ );
51
+ });
52
+
53
+ test("self-hosted gitlab baseUrl strips /api/v4", () => {
54
+ expect(
55
+ deriveSourceUrl({
56
+ providerType: "gitlab",
57
+ baseUrl: "https://gitlab.example.com/api/v4",
58
+ repository: "team/sub/checkstack",
59
+ filePath: "catalog/system.yaml",
60
+ }),
61
+ ).toBe(
62
+ "https://gitlab.example.com/team/sub/checkstack/-/blob/HEAD/catalog/system.yaml",
63
+ );
64
+ });
65
+
66
+ test("nested gitlab namespaces preserved", () => {
67
+ expect(
68
+ deriveSourceUrl({
69
+ providerType: "gitlab",
70
+ baseUrl: null,
71
+ repository: "group/sub/project",
72
+ filePath: "a/b.yaml",
73
+ }),
74
+ ).toBe("https://gitlab.com/group/sub/project/-/blob/HEAD/a/b.yaml");
75
+ });
76
+
77
+ test("encodes spaces and special characters in path segments", () => {
78
+ expect(
79
+ deriveSourceUrl({
80
+ providerType: "github",
81
+ baseUrl: null,
82
+ repository: "acme/checkstack",
83
+ filePath: "catalog/my system.yaml",
84
+ }),
85
+ ).toBe(
86
+ "https://github.com/acme/checkstack/blob/HEAD/catalog/my%20system.yaml",
87
+ );
88
+ });
89
+
90
+ test("strips leading slashes from filePath", () => {
91
+ expect(
92
+ deriveSourceUrl({
93
+ providerType: "github",
94
+ baseUrl: null,
95
+ repository: "acme/checkstack",
96
+ filePath: "/catalog/system.yaml",
97
+ }),
98
+ ).toBe("https://github.com/acme/checkstack/blob/HEAD/catalog/system.yaml");
99
+ });
100
+
101
+ test("returns null for unknown self-hosted baseUrl", () => {
102
+ expect(
103
+ deriveSourceUrl({
104
+ providerType: "github",
105
+ baseUrl: "https://github.example.com/some/weird/path",
106
+ repository: "acme/checkstack",
107
+ filePath: "catalog/system.yaml",
108
+ }),
109
+ ).toBeNull();
110
+ });
111
+
112
+ test("returns null for traversal attempts", () => {
113
+ expect(
114
+ deriveSourceUrl({
115
+ providerType: "github",
116
+ baseUrl: null,
117
+ repository: "acme/checkstack",
118
+ filePath: "../etc/passwd",
119
+ }),
120
+ ).toBeNull();
121
+ });
122
+
123
+ test("returns null for empty inputs", () => {
124
+ expect(
125
+ deriveSourceUrl({
126
+ providerType: "github",
127
+ baseUrl: null,
128
+ repository: "",
129
+ filePath: "x.yaml",
130
+ }),
131
+ ).toBeNull();
132
+ expect(
133
+ deriveSourceUrl({
134
+ providerType: "github",
135
+ baseUrl: null,
136
+ repository: "a/b",
137
+ filePath: "",
138
+ }),
139
+ ).toBeNull();
140
+ });
141
+
142
+ test("returns null for malformed baseUrl", () => {
143
+ expect(
144
+ deriveSourceUrl({
145
+ providerType: "github",
146
+ baseUrl: "not a url",
147
+ repository: "acme/checkstack",
148
+ filePath: "x.yaml",
149
+ }),
150
+ ).toBeNull();
151
+ });
152
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Derive a human-browsable source URL for a GitOps-managed entity.
3
+ *
4
+ * Returns null when we don't have enough information to construct a stable URL
5
+ * (e.g., unknown provider type or a malformed `repository` value). Callers
6
+ * should fall back to displaying the raw `repository`/`filePath` text.
7
+ *
8
+ * The URL points at the file on the default branch via the `HEAD` ref so we
9
+ * don't need to know the branch name. Both GitHub and GitLab resolve `HEAD`
10
+ * server-side for blob URLs.
11
+ */
12
+ export interface DeriveSourceUrlInput {
13
+ providerType: "github" | "gitlab";
14
+ /** The provider's `baseUrl` (API base) — null for public github.com/gitlab.com. */
15
+ baseUrl: string | null;
16
+ /** GitHub: `owner/repo`. GitLab: `group/project` (or nested `group/sub/project`). */
17
+ repository: string;
18
+ filePath: string;
19
+ }
20
+
21
+ const GITHUB_PUBLIC_HOST = "https://github.com";
22
+ const GITLAB_PUBLIC_HOST = "https://gitlab.com";
23
+
24
+ const stripApiSuffix = ({
25
+ providerType,
26
+ baseUrl,
27
+ }: {
28
+ providerType: "github" | "gitlab";
29
+ baseUrl: string;
30
+ }): string | null => {
31
+ // We only know how to strip the well-known API path suffixes. Anything else
32
+ // we treat as untrusted and bail out — better to fall back to text than to
33
+ // construct a broken link.
34
+ try {
35
+ const url = new URL(baseUrl);
36
+ if (providerType === "github") {
37
+ // api.github.com → github.com (public host has no API path component)
38
+ if (url.host === "api.github.com") return GITHUB_PUBLIC_HOST;
39
+ // GitHub Enterprise: https://github.example.com/api/v3
40
+ if (url.pathname.replace(/\/$/, "") === "/api/v3") {
41
+ return `${url.protocol}//${url.host}`;
42
+ }
43
+ return null;
44
+ }
45
+ // GitLab: https://gitlab.example.com/api/v4
46
+ if (url.pathname.replace(/\/$/, "") === "/api/v4") {
47
+ return `${url.protocol}//${url.host}`;
48
+ }
49
+ return null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ };
54
+
55
+ const encodeFilePath = (filePath: string) =>
56
+ filePath
57
+ .replace(/^\/+/, "")
58
+ .split("/")
59
+ .map((segment) => encodeURIComponent(segment))
60
+ .join("/");
61
+
62
+ export const deriveSourceUrl = ({
63
+ providerType,
64
+ baseUrl,
65
+ repository,
66
+ filePath,
67
+ }: DeriveSourceUrlInput): string | null => {
68
+ if (!repository || !filePath) return null;
69
+ // Reject backslashes and absolute-looking paths to keep things tidy.
70
+ if (repository.includes("..") || filePath.includes("..")) return null;
71
+
72
+ const host = baseUrl
73
+ ? stripApiSuffix({ providerType, baseUrl })
74
+ : providerType === "github"
75
+ ? GITHUB_PUBLIC_HOST
76
+ : GITLAB_PUBLIC_HOST;
77
+ if (!host) return null;
78
+
79
+ const repoPath = repository
80
+ .split("/")
81
+ .map((segment) => encodeURIComponent(segment))
82
+ .join("/");
83
+ const encodedFile = encodeFilePath(filePath);
84
+
85
+ if (providerType === "github") {
86
+ return `${host}/${repoPath}/blob/HEAD/${encodedFile}`;
87
+ }
88
+ return `${host}/${repoPath}/-/blob/HEAD/${encodedFile}`;
89
+ };
package/tsconfig.json CHANGED
@@ -1,4 +1,11 @@
1
1
  {
2
2
  "extends": "@checkstack/tsconfig/common.json",
3
- "include": ["src"]
3
+ "include": [
4
+ "src"
5
+ ],
6
+ "references": [
7
+ {
8
+ "path": "../common"
9
+ }
10
+ ]
4
11
  }