Commite2da5c6unknown_key
feat(BLOCK-C2+C3+C4): schema + migrations for packages, pages, environments
feat(BLOCK-C2+C3+C4): schema + migrations for packages, pages, environments - 0009: packages, package_versions, package_tags (npm-compatible). Unique (ecosystem, scope, name); unique (package_id, version); unique (package_id, tag). Tarball stored as bytea in DB (text/base64 in drizzle until a migration swaps the type). - 0010: pages_deployments (per-push snapshot) + pages_settings (one-row per repo: enabled, source_branch, source_dir, custom_domain). - 0011: environments (name, require_approval, reviewers JSON, wait_timer, allowed_branches JSON) + deployment_approvals (decision, comment). Lib + routes + wiring land in follow-up commits per block.
4 files changed+302−0e2da5c60a071adc83a2cd75b1e67180536a799f9
4 changed files+302−0
Addeddrizzle/0009_packages.sql+65−0View fileUnifiedSplit
@@ -0,0 +1,65 @@
1-- Gluecron migration 0009: Block C2 — Package registry (npm-compatible).
2--
3-- Tables:
4-- packages — logical package (one per repo per ecosystem)
5-- package_versions — published versions with tarball stored as bytea
6-- package_tags — dist tags (latest, beta, etc.)
7
8--> statement-breakpoint
9CREATE TABLE IF NOT EXISTS "packages" (
10 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
11 "repository_id" uuid NOT NULL,
12 "ecosystem" text NOT NULL DEFAULT 'npm',
13 "scope" text,
14 "name" text NOT NULL,
15 "description" text,
16 "readme" text,
17 "homepage" text,
18 "license" text,
19 "visibility" text NOT NULL DEFAULT 'public',
20 "created_at" timestamp DEFAULT now() NOT NULL,
21 "updated_at" timestamp DEFAULT now() NOT NULL,
22 CONSTRAINT "packages_repo_fk" FOREIGN KEY ("repository_id") REFERENCES "repositories"("id") ON DELETE cascade
23);
24
25--> statement-breakpoint
26CREATE INDEX IF NOT EXISTS "packages_repo" ON "packages" ("repository_id");
27--> statement-breakpoint
28CREATE UNIQUE INDEX IF NOT EXISTS "packages_eco_scope_name" ON "packages" ("ecosystem", "scope", "name");
29
30--> statement-breakpoint
31CREATE TABLE IF NOT EXISTS "package_versions" (
32 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
33 "package_id" uuid NOT NULL,
34 "version" text NOT NULL,
35 "shasum" text NOT NULL,
36 "integrity" text,
37 "size_bytes" integer NOT NULL DEFAULT 0,
38 "metadata" text NOT NULL DEFAULT '{}',
39 "tarball" text,
40 "published_by" uuid,
41 "yanked" boolean NOT NULL DEFAULT false,
42 "yanked_reason" text,
43 "published_at" timestamp DEFAULT now() NOT NULL,
44 CONSTRAINT "package_versions_pkg_fk" FOREIGN KEY ("package_id") REFERENCES "packages"("id") ON DELETE cascade,
45 CONSTRAINT "package_versions_user_fk" FOREIGN KEY ("published_by") REFERENCES "users"("id") ON DELETE set null
46);
47
48--> statement-breakpoint
49CREATE INDEX IF NOT EXISTS "package_versions_pkg" ON "package_versions" ("package_id");
50--> statement-breakpoint
51CREATE UNIQUE INDEX IF NOT EXISTS "package_versions_pkg_version" ON "package_versions" ("package_id", "version");
52
53--> statement-breakpoint
54CREATE TABLE IF NOT EXISTS "package_tags" (
55 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
56 "package_id" uuid NOT NULL,
57 "tag" text NOT NULL,
58 "version_id" uuid NOT NULL,
59 "updated_at" timestamp DEFAULT now() NOT NULL,
60 CONSTRAINT "package_tags_pkg_fk" FOREIGN KEY ("package_id") REFERENCES "packages"("id") ON DELETE cascade,
61 CONSTRAINT "package_tags_version_fk" FOREIGN KEY ("version_id") REFERENCES "package_versions"("id") ON DELETE cascade
62);
63
64--> statement-breakpoint
65CREATE UNIQUE INDEX IF NOT EXISTS "package_tags_pkg_tag" ON "package_tags" ("package_id", "tag");
Addeddrizzle/0010_pages.sql+34−0View fileUnifiedSplit
@@ -0,0 +1,34 @@
1-- Gluecron migration 0010: Block C3 — Pages / static hosting.
2--
3-- Tables:
4-- pages_deployments — recorded every time the source branch advances
5-- pages_settings — per-repo pages config (enabled, source branch/dir, custom domain)
6
7--> statement-breakpoint
8CREATE TABLE IF NOT EXISTS "pages_deployments" (
9 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
10 "repository_id" uuid NOT NULL,
11 "ref" text NOT NULL DEFAULT 'refs/heads/gh-pages',
12 "commit_sha" text NOT NULL,
13 "status" text NOT NULL DEFAULT 'success',
14 "triggered_by" uuid,
15 "created_at" timestamp DEFAULT now() NOT NULL,
16 CONSTRAINT "pages_deployments_repo_fk" FOREIGN KEY ("repository_id") REFERENCES "repositories"("id") ON DELETE cascade,
17 CONSTRAINT "pages_deployments_user_fk" FOREIGN KEY ("triggered_by") REFERENCES "users"("id") ON DELETE set null
18);
19
20--> statement-breakpoint
21CREATE INDEX IF NOT EXISTS "pages_deployments_repo" ON "pages_deployments" ("repository_id");
22--> statement-breakpoint
23CREATE INDEX IF NOT EXISTS "pages_deployments_created" ON "pages_deployments" ("created_at");
24
25--> statement-breakpoint
26CREATE TABLE IF NOT EXISTS "pages_settings" (
27 "repository_id" uuid PRIMARY KEY NOT NULL,
28 "enabled" boolean NOT NULL DEFAULT true,
29 "source_branch" text NOT NULL DEFAULT 'gh-pages',
30 "source_dir" text NOT NULL DEFAULT '/',
31 "custom_domain" text,
32 "updated_at" timestamp DEFAULT now() NOT NULL,
33 CONSTRAINT "pages_settings_repo_fk" FOREIGN KEY ("repository_id") REFERENCES "repositories"("id") ON DELETE cascade
34);
Addeddrizzle/0011_environments.sql+37−0View fileUnifiedSplit
@@ -0,0 +1,37 @@
1-- Gluecron migration 0011: Block C4 — Environments with protected approvals.
2--
3-- Tables:
4-- environments — per-repo named environments (production, staging, preview)
5-- deployment_approvals — approve/reject decisions on a pending deployment
6
7--> statement-breakpoint
8CREATE TABLE IF NOT EXISTS "environments" (
9 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
10 "repository_id" uuid NOT NULL,
11 "name" text NOT NULL,
12 "require_approval" boolean NOT NULL DEFAULT false,
13 "reviewers" text NOT NULL DEFAULT '[]',
14 "wait_timer_minutes" integer NOT NULL DEFAULT 0,
15 "allowed_branches" text NOT NULL DEFAULT '[]',
16 "created_at" timestamp DEFAULT now() NOT NULL,
17 "updated_at" timestamp DEFAULT now() NOT NULL,
18 CONSTRAINT "environments_repo_fk" FOREIGN KEY ("repository_id") REFERENCES "repositories"("id") ON DELETE cascade
19);
20
21--> statement-breakpoint
22CREATE UNIQUE INDEX IF NOT EXISTS "environments_repo_name" ON "environments" ("repository_id", "name");
23
24--> statement-breakpoint
25CREATE TABLE IF NOT EXISTS "deployment_approvals" (
26 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
27 "deployment_id" uuid NOT NULL,
28 "user_id" uuid NOT NULL,
29 "decision" text NOT NULL,
30 "comment" text,
31 "created_at" timestamp DEFAULT now() NOT NULL,
32 CONSTRAINT "deployment_approvals_deployment_fk" FOREIGN KEY ("deployment_id") REFERENCES "deployments"("id") ON DELETE cascade,
33 CONSTRAINT "deployment_approvals_user_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade
34);
35
36--> statement-breakpoint
37CREATE INDEX IF NOT EXISTS "deployment_approvals_deployment" ON "deployment_approvals" ("deployment_id");
Modifiedsrc/db/schema.ts+166−0View fileUnifiedSplit
@@ -1095,3 +1095,169 @@ export type Workflow = typeof workflows.$inferSelect;
10951095export type WorkflowRun = typeof workflowRuns.$inferSelect;
10961096export type WorkflowJob = typeof workflowJobs.$inferSelect;
10971097export type WorkflowArtifact = typeof workflowArtifacts.$inferSelect;
1098
1099// ---------------------------------------------------------------------------
1100// Block C2 — Package registry (npm-compatible)
1101// ---------------------------------------------------------------------------
1102
1103export const packages = pgTable(
1104 "packages",
1105 {
1106 id: uuid("id").primaryKey().defaultRandom(),
1107 repositoryId: uuid("repository_id")
1108 .notNull()
1109 .references(() => repositories.id, { onDelete: "cascade" }),
1110 ecosystem: text("ecosystem").notNull().default("npm"), // "npm" | "container"
1111 scope: text("scope"), // "@acme" for npm; null for unscoped
1112 name: text("name").notNull(), // "my-lib" (without scope)
1113 description: text("description"),
1114 readme: text("readme"),
1115 homepage: text("homepage"),
1116 license: text("license"),
1117 visibility: text("visibility").notNull().default("public"), // "public" | "private"
1118 createdAt: timestamp("created_at").defaultNow().notNull(),
1119 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1120 },
1121 (table) => [
1122 index("packages_repo").on(table.repositoryId),
1123 uniqueIndex("packages_eco_scope_name").on(
1124 table.ecosystem,
1125 table.scope,
1126 table.name
1127 ),
1128 ]
1129);
1130
1131export const packageVersions = pgTable(
1132 "package_versions",
1133 {
1134 id: uuid("id").primaryKey().defaultRandom(),
1135 packageId: uuid("package_id")
1136 .notNull()
1137 .references(() => packages.id, { onDelete: "cascade" }),
1138 version: text("version").notNull(), // "1.2.3"
1139 shasum: text("shasum").notNull(), // sha1 (for npm compat) hex
1140 integrity: text("integrity"), // "sha512-..." base64
1141 sizeBytes: integer("size_bytes").default(0).notNull(),
1142 metadata: text("metadata").notNull().default("{}"), // package.json JSON
1143 tarball: text("tarball"), // base64-encoded; bytea in migration
1144 publishedBy: uuid("published_by").references(() => users.id, {
1145 onDelete: "set null",
1146 }),
1147 yanked: boolean("yanked").default(false).notNull(),
1148 yankedReason: text("yanked_reason"),
1149 publishedAt: timestamp("published_at").defaultNow().notNull(),
1150 },
1151 (table) => [
1152 index("package_versions_pkg").on(table.packageId),
1153 uniqueIndex("package_versions_pkg_version").on(table.packageId, table.version),
1154 ]
1155);
1156
1157export const packageTags = pgTable(
1158 "package_tags",
1159 {
1160 id: uuid("id").primaryKey().defaultRandom(),
1161 packageId: uuid("package_id")
1162 .notNull()
1163 .references(() => packages.id, { onDelete: "cascade" }),
1164 tag: text("tag").notNull(), // "latest" | "beta" | ...
1165 versionId: uuid("version_id")
1166 .notNull()
1167 .references(() => packageVersions.id, { onDelete: "cascade" }),
1168 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1169 },
1170 (table) => [
1171 uniqueIndex("package_tags_pkg_tag").on(table.packageId, table.tag),
1172 ]
1173);
1174
1175export type Package = typeof packages.$inferSelect;
1176export type PackageVersion = typeof packageVersions.$inferSelect;
1177export type PackageTag = typeof packageTags.$inferSelect;
1178
1179// ---------------------------------------------------------------------------
1180// Block C3 — Pages / static hosting
1181// ---------------------------------------------------------------------------
1182
1183export const pagesDeployments = pgTable(
1184 "pages_deployments",
1185 {
1186 id: uuid("id").primaryKey().defaultRandom(),
1187 repositoryId: uuid("repository_id")
1188 .notNull()
1189 .references(() => repositories.id, { onDelete: "cascade" }),
1190 ref: text("ref").notNull().default("refs/heads/gh-pages"),
1191 commitSha: text("commit_sha").notNull(),
1192 status: text("status").notNull().default("success"), // "success" | "failed"
1193 triggeredBy: uuid("triggered_by").references(() => users.id, {
1194 onDelete: "set null",
1195 }),
1196 createdAt: timestamp("created_at").defaultNow().notNull(),
1197 },
1198 (table) => [
1199 index("pages_deployments_repo").on(table.repositoryId),
1200 index("pages_deployments_created").on(table.createdAt),
1201 ]
1202);
1203
1204export const pagesSettings = pgTable("pages_settings", {
1205 repositoryId: uuid("repository_id")
1206 .primaryKey()
1207 .references(() => repositories.id, { onDelete: "cascade" }),
1208 enabled: boolean("enabled").default(true).notNull(),
1209 sourceBranch: text("source_branch").notNull().default("gh-pages"),
1210 sourceDir: text("source_dir").notNull().default("/"), // e.g. "/" or "/docs"
1211 customDomain: text("custom_domain"),
1212 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1213});
1214
1215export type PagesDeployment = typeof pagesDeployments.$inferSelect;
1216export type PagesSettings = typeof pagesSettings.$inferSelect;
1217
1218// ---------------------------------------------------------------------------
1219// Block C4 — Environments with protected approvals
1220// ---------------------------------------------------------------------------
1221
1222export const environments = pgTable(
1223 "environments",
1224 {
1225 id: uuid("id").primaryKey().defaultRandom(),
1226 repositoryId: uuid("repository_id")
1227 .notNull()
1228 .references(() => repositories.id, { onDelete: "cascade" }),
1229 name: text("name").notNull(), // "production" | "staging" | "preview"
1230 requireApproval: boolean("require_approval").default(false).notNull(),
1231 // JSON array of user IDs that can approve deploys.
1232 reviewers: text("reviewers").notNull().default("[]"),
1233 waitTimerMinutes: integer("wait_timer_minutes").default(0).notNull(),
1234 allowedBranches: text("allowed_branches").notNull().default("[]"), // JSON glob patterns
1235 createdAt: timestamp("created_at").defaultNow().notNull(),
1236 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1237 },
1238 (table) => [
1239 uniqueIndex("environments_repo_name").on(table.repositoryId, table.name),
1240 ]
1241);
1242
1243export const deploymentApprovals = pgTable(
1244 "deployment_approvals",
1245 {
1246 id: uuid("id").primaryKey().defaultRandom(),
1247 deploymentId: uuid("deployment_id")
1248 .notNull()
1249 .references(() => deployments.id, { onDelete: "cascade" }),
1250 userId: uuid("user_id")
1251 .notNull()
1252 .references(() => users.id, { onDelete: "cascade" }),
1253 decision: text("decision").notNull(), // "approved" | "rejected"
1254 comment: text("comment"),
1255 createdAt: timestamp("created_at").defaultNow().notNull(),
1256 },
1257 (table) => [
1258 index("deployment_approvals_deployment").on(table.deploymentId),
1259 ]
1260);
1261
1262export type Environment = typeof environments.$inferSelect;
1263export type DeploymentApproval = typeof deploymentApprovals.$inferSelect;
10981264