Skip to content

Commit 3484711

Browse files
authored
Merge pull request #28 from willwearing/fix/remove-vercel-deploy-hook
Remove Vercel deploy hook — use git integration
2 parents ad9ee54 + 2fa488f commit 3484711

12 files changed

Lines changed: 537 additions & 296 deletions

File tree

.github/workflows/ci-deploy.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ jobs:
4242
- name: Run frontend tests
4343
run: cd apps/web && bun run test
4444

45-
deploy:
46-
name: Deploy
45+
deploy-backend:
46+
name: Deploy Backend
4747
needs: test
4848
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
4949
runs-on: ubuntu-latest
@@ -58,5 +58,4 @@ jobs:
5858
env:
5959
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
6060

61-
- name: Deploy frontend to Vercel
62-
run: curl -s -X POST "${{ secrets.VERCEL_DEPLOY_HOOK }}"
61+
# Frontend deploys automatically via Vercel git integration
Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,6 @@
1-
import { z } from 'zod';
2-
3-
const AcademyMetaSchema = z.object({
4-
id: z.string().min(1),
5-
name: z.string().min(1),
6-
description: z.string().optional(),
7-
version: z.string().min(1),
8-
});
9-
10-
const AcademyPartSchema = z.object({
11-
id: z.string().min(1),
12-
name: z.string().min(1),
13-
description: z.string().optional(),
14-
});
15-
16-
const AcademyCourseEntrySchema = z.object({
17-
id: z.string().min(1),
18-
name: z.string().min(1),
19-
description: z.string().optional(),
20-
part: z.string().min(1).optional(),
21-
file: z.string().min(1),
22-
});
23-
24-
export const AcademyManifestSchema = z
25-
.object({
26-
academy: AcademyMetaSchema,
27-
parts: z.array(AcademyPartSchema).default([]),
28-
courses: z.array(AcademyCourseEntrySchema).min(1),
29-
})
30-
.superRefine((data, ctx) => {
31-
const partIds = new Set<string>();
32-
for (const part of data.parts) {
33-
if (partIds.has(part.id)) {
34-
ctx.addIssue({
35-
code: z.ZodIssueCode.custom,
36-
message: `Duplicate academy part "${part.id}"`,
37-
});
38-
continue;
39-
}
40-
partIds.add(part.id);
41-
}
42-
43-
const courseIds = new Set<string>();
44-
const courseFiles = new Set<string>();
45-
for (const course of data.courses) {
46-
if (courseIds.has(course.id)) {
47-
ctx.addIssue({
48-
code: z.ZodIssueCode.custom,
49-
message: `Duplicate academy course "${course.id}"`,
50-
});
51-
} else {
52-
courseIds.add(course.id);
53-
}
54-
55-
if (courseFiles.has(course.file)) {
56-
ctx.addIssue({
57-
code: z.ZodIssueCode.custom,
58-
message: `Duplicate academy course file "${course.file}"`,
59-
});
60-
} else {
61-
courseFiles.add(course.file);
62-
}
63-
64-
if (course.part && !partIds.has(course.part)) {
65-
ctx.addIssue({
66-
code: z.ZodIssueCode.custom,
67-
message: `Course "${course.id}" references unknown academy part "${course.part}"`,
68-
});
69-
}
70-
}
71-
});
72-
73-
export type AcademyManifest = z.infer<typeof AcademyManifestSchema>;
74-
export type AcademyManifestPart = z.infer<typeof AcademyPartSchema>;
75-
export type AcademyManifestCourse = z.infer<typeof AcademyCourseEntrySchema>;
1+
export {
2+
AcademyManifestSchema,
3+
type AcademyManifest,
4+
type AcademyManifestPart,
5+
type AcademyManifestCourse,
6+
} from '@graspful/shared';
Lines changed: 11 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1,201 +1,11 @@
1-
import { z } from 'zod';
2-
3-
const ContentBlockYamlSchema = z.discriminatedUnion('type', [
4-
z.object({
5-
type: z.literal('image'),
6-
url: z.string().min(1),
7-
alt: z.string().min(1),
8-
caption: z.string().optional(),
9-
width: z.number().int().positive().optional(),
10-
}),
11-
z.object({
12-
type: z.literal('video'),
13-
url: z.string().min(1),
14-
title: z.string().min(1),
15-
caption: z.string().optional(),
16-
}),
17-
z.object({
18-
type: z.literal('link'),
19-
url: z.string().min(1),
20-
title: z.string().min(1),
21-
description: z.string().optional(),
22-
}),
23-
z.object({
24-
type: z.literal('callout'),
25-
title: z.string().min(1),
26-
body: z.string().min(1),
27-
}),
28-
]);
29-
30-
const ProblemYamlSchema = z.object({
31-
id: z.string(),
32-
type: z.enum([
33-
'multiple_choice',
34-
'fill_blank',
35-
'true_false',
36-
'ordering',
37-
'matching',
38-
'scenario',
39-
]),
40-
question: z.string(),
41-
options: z.array(z.string()).optional(),
42-
correct: z.union([z.number(), z.string()]),
43-
explanation: z.string().optional(),
44-
difficulty: z.number().int().min(1).max(5).optional(),
45-
});
46-
47-
const KnowledgePointYamlSchema = z.object({
48-
id: z.string(),
49-
instruction: z.string().optional(),
50-
instructionContent: z.array(ContentBlockYamlSchema).optional().default([]),
51-
workedExample: z.string().optional(),
52-
workedExampleContent: z.array(ContentBlockYamlSchema).optional().default([]),
53-
problems: z.array(ProblemYamlSchema).optional().default([]),
54-
});
55-
56-
const EncompassingRefSchema = z.object({
57-
concept: z.string(),
58-
weight: z.number().min(0).max(1),
59-
});
60-
61-
const ConceptYamlSchema = z.object({
62-
id: z.string(),
63-
name: z.string(),
64-
section: z.string().optional(),
65-
difficulty: z.number().int().min(1).max(10),
66-
estimatedMinutes: z.number().int().positive(),
67-
tags: z.array(z.string()).default([]),
68-
sourceRef: z.string().optional(),
69-
prerequisites: z.array(z.string()).optional().default([]),
70-
encompassing: z.array(EncompassingRefSchema).optional().default([]),
71-
knowledgePoints: z.array(KnowledgePointYamlSchema).optional().default([]),
72-
});
73-
74-
const SectionExamBlueprintYamlSchema = z.object({
75-
conceptId: z.string(),
76-
minQuestions: z.number().int().positive(),
77-
});
78-
79-
const SectionExamYamlSchema = z.object({
80-
enabled: z.boolean().default(true),
81-
passingScore: z.number().min(0).max(1).default(0.75),
82-
timeLimitMinutes: z.number().int().positive().optional(),
83-
questionCount: z.number().int().positive().default(10),
84-
blueprint: z.array(SectionExamBlueprintYamlSchema).default([]),
85-
instructions: z.string().optional(),
86-
});
87-
88-
const SectionYamlSchema = z.object({
89-
id: z.string(),
90-
name: z.string(),
91-
description: z.string().optional(),
92-
sectionExam: SectionExamYamlSchema.optional(),
93-
});
94-
95-
const CourseMetaSchema = z.object({
96-
id: z.string(),
97-
name: z.string(),
98-
description: z.string().optional(),
99-
estimatedHours: z.number().positive(),
100-
version: z.string(),
101-
sourceDocument: z.string().optional(),
102-
});
103-
104-
export const CourseYamlSchema = z
105-
.object({
106-
course: CourseMetaSchema,
107-
sections: z.array(SectionYamlSchema).optional().default([]),
108-
concepts: z.array(ConceptYamlSchema),
109-
})
110-
.superRefine((data, ctx) => {
111-
const conceptIds = new Set(data.concepts.map((concept) => concept.id));
112-
const sectionIds = new Set(data.sections.map((section) => section.id));
113-
const conceptToSection = new Map(
114-
data.concepts
115-
.filter((concept) => concept.section)
116-
.map((concept) => [concept.id, concept.section as string]),
117-
);
118-
119-
for (const concept of data.concepts) {
120-
if (concept.section && !sectionIds.has(concept.section)) {
121-
ctx.addIssue({
122-
code: z.ZodIssueCode.custom,
123-
message: `Concept "${concept.id}" references unknown section "${concept.section}"`,
124-
});
125-
}
126-
}
127-
128-
for (const section of data.sections) {
129-
const exam = section.sectionExam;
130-
if (!exam?.enabled) {
131-
continue;
132-
}
133-
134-
const sectionConcepts = data.concepts.filter(
135-
(concept) => concept.section === section.id,
136-
);
137-
138-
if (sectionConcepts.length < 2) {
139-
ctx.addIssue({
140-
code: z.ZodIssueCode.custom,
141-
message: `Section "${section.id}" needs at least two concepts when sectionExam is enabled`,
142-
});
143-
}
144-
145-
const minQuestions = exam.blueprint.reduce(
146-
(sum, item) => sum + item.minQuestions,
147-
0,
148-
);
149-
if (exam.questionCount < minQuestions) {
150-
ctx.addIssue({
151-
code: z.ZodIssueCode.custom,
152-
message: `Section "${section.id}" questionCount must be at least the sum of blueprint minQuestions`,
153-
});
154-
}
155-
156-
const availableProblems = sectionConcepts.reduce(
157-
(sum, concept) =>
158-
sum +
159-
concept.knowledgePoints.reduce(
160-
(kpSum, kp) => kpSum + kp.problems.length,
161-
0,
162-
),
163-
0,
164-
);
165-
166-
if (availableProblems < exam.questionCount) {
167-
ctx.addIssue({
168-
code: z.ZodIssueCode.custom,
169-
message: `Section "${section.id}" does not have enough eligible problems for its section exam`,
170-
});
171-
}
172-
173-
for (const item of exam.blueprint) {
174-
if (!conceptIds.has(item.conceptId)) {
175-
ctx.addIssue({
176-
code: z.ZodIssueCode.custom,
177-
message: `Section "${section.id}" blueprint references unknown concept "${item.conceptId}"`,
178-
});
179-
continue;
180-
}
181-
182-
if (conceptToSection.get(item.conceptId) !== section.id) {
183-
ctx.addIssue({
184-
code: z.ZodIssueCode.custom,
185-
message: `Section "${section.id}" blueprint concept "${item.conceptId}" must belong to the same section`,
186-
});
187-
}
188-
}
189-
}
190-
});
191-
192-
export type CourseYaml = z.infer<typeof CourseYamlSchema>;
193-
export type SectionYaml = z.infer<typeof SectionYamlSchema>;
194-
export type SectionExamYaml = z.infer<typeof SectionExamYamlSchema>;
195-
export type SectionExamBlueprintYaml = z.infer<
196-
typeof SectionExamBlueprintYamlSchema
197-
>;
198-
export type ConceptYaml = z.infer<typeof ConceptYamlSchema>;
199-
export type KnowledgePointYaml = z.infer<typeof KnowledgePointYamlSchema>;
200-
export type ProblemYaml = z.infer<typeof ProblemYamlSchema>;
201-
export type ContentBlockYaml = z.infer<typeof ContentBlockYamlSchema>;
1+
export {
2+
CourseYamlSchema,
3+
type CourseYaml,
4+
type SectionYaml,
5+
type SectionExamYaml,
6+
type SectionExamBlueprintYaml,
7+
type ConceptYaml,
8+
type KnowledgePointYaml,
9+
type ProblemYaml,
10+
type ContentBlockYaml,
11+
} from '@graspful/shared';

0 commit comments

Comments
 (0)