@learnpack/learnpack 5.0.353 → 5.0.355
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/lib/commands/serve.js +1 -1
- package/lib/scripts/descriptionsS3Backfill.js +142 -22
- package/lib/utils/awsCredentials.d.ts +20 -0
- package/lib/utils/awsCredentials.js +43 -0
- package/lib/utils/descriptions/backfillEvents.d.ts +60 -0
- package/lib/utils/descriptions/backfillEvents.js +107 -0
- package/lib/utils/descriptions/generateCourseDescriptions.d.ts +7 -0
- package/lib/utils/descriptions/generateCourseDescriptions.js +3 -0
- package/lib/utils/descriptions/publishStage.js +14 -3
- package/lib/utils/descriptions/resumePublication.js +17 -2
- package/lib/utils/descriptions/s3Storage.js +13 -6
- package/lib/utils/packageManifest.d.ts +8 -0
- package/lib/utils/packageManifest.js +8 -0
- package/lib/utils/repair/legacyPackageRepair.d.ts +131 -0
- package/lib/utils/repair/legacyPackageRepair.js +492 -0
- package/lib/utils/repair/repairStorage.d.ts +68 -0
- package/lib/utils/repair/repairStorage.js +89 -0
- package/lib/utils/s3/packageManifestBackfill.js +3 -8
- package/lib/utils/s3/packageSourcesAudit.d.ts +75 -0
- package/lib/utils/s3/packageSourcesAudit.js +184 -0
- package/package.json +3 -1
- package/src/commands/serve.ts +1 -1
- package/src/scripts/README.md +244 -0
- package/src/scripts/descriptionsS3Backfill.ts +188 -20
- package/src/utils/awsCredentials.ts +57 -0
- package/src/utils/descriptions/backfillEvents.ts +152 -0
- package/src/utils/descriptions/generateCourseDescriptions.ts +10 -0
- package/src/utils/descriptions/publishStage.ts +394 -382
- package/src/utils/descriptions/resumePublication.ts +217 -200
- package/src/utils/descriptions/s3Storage.ts +214 -206
- package/src/utils/packageManifest.ts +8 -1
- package/src/utils/repair/legacyPackageRepair.ts +731 -0
- package/src/utils/repair/repairStorage.ts +168 -0
- package/src/utils/s3/packageManifestBackfill.ts +771 -776
- package/src/utils/s3/packageSourcesAudit.ts +311 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid"
|
|
2
|
+
import { PackageManifest } from "../packageManifest"
|
|
3
|
+
import {
|
|
4
|
+
buildPackageManifestUpdatedEvent,
|
|
5
|
+
fetchPackageInfo,
|
|
6
|
+
sendPublishEvent,
|
|
7
|
+
} from "../publishEvents"
|
|
8
|
+
import {
|
|
9
|
+
CourseDescriptionsStorage,
|
|
10
|
+
GenerateCourseDescriptionsResult,
|
|
11
|
+
} from "./generateCourseDescriptions"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `package_manifest_updated` for courses reconciled by the backfill.
|
|
15
|
+
*
|
|
16
|
+
* The backfill regenerates descriptions of packages that are already published,
|
|
17
|
+
* so breathecode needs the same notification a publication sends — but there is
|
|
18
|
+
* no publication here: no `package_published` precedes this event, and no
|
|
19
|
+
* journal tracks it (the sweep resumes publications, and a backfill is simply
|
|
20
|
+
* re-run instead).
|
|
21
|
+
*
|
|
22
|
+
* That is what makes `publish_id` awkward. The field exists to correlate the two
|
|
23
|
+
* events of one publication, a pair this event has no half of. Since it is
|
|
24
|
+
* required, the value has to be something that cannot be mistaken for a real
|
|
25
|
+
* publication: a bare uuid — the obvious choice — would be exactly that mistake,
|
|
26
|
+
* and if breathecode ever starts pairing events it would search forever for a
|
|
27
|
+
* `package_published` that was never sent.
|
|
28
|
+
*
|
|
29
|
+
* Hence `backfill-{runId}-{slug}`:
|
|
30
|
+
*
|
|
31
|
+
* - the `backfill-` prefix makes the origin readable in the webhook log and
|
|
32
|
+
* discriminable in code (`isBackfillPublishId`); real ids are bare uuids from
|
|
33
|
+
* `createJournal`, so the two spaces cannot collide;
|
|
34
|
+
* - `{slug}` keeps it unique per event, which a per-run id alone would not be;
|
|
35
|
+
* - a `{runId}` shared by every course of one invocation makes "every event from
|
|
36
|
+
* the run I fired at 15:40" a single substring query, which is the question
|
|
37
|
+
* you actually ask when the catalogue is backfilled in small batches.
|
|
38
|
+
*
|
|
39
|
+
* Encoding this in `publish_id`, rather than adding an `origin` field, keeps the
|
|
40
|
+
* event contract untouched: breathecode ignores `publish_id` today, so this
|
|
41
|
+
* costs them nothing and needs no coordination. If they ever start consuming the
|
|
42
|
+
* provenance, it can be promoted to a field of its own.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export const BACKFILL_PUBLISH_ID_PREFIX = "backfill-"
|
|
46
|
+
|
|
47
|
+
/** One per script invocation, shared by every course it touches. */
|
|
48
|
+
export function newBackfillRunId(): string {
|
|
49
|
+
return uuidv4()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function backfillPublishId(runId: string, courseSlug: string): string {
|
|
53
|
+
return `${BACKFILL_PUBLISH_ID_PREFIX}${runId}-${courseSlug}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isBackfillPublishId(publishId: string): boolean {
|
|
57
|
+
return publishId.startsWith(BACKFILL_PUBLISH_ID_PREFIX)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `skipped` means the event was never attempted, which is not a failure: a
|
|
62
|
+
* course the backfill did not change has nothing to announce.
|
|
63
|
+
*/
|
|
64
|
+
export type BackfillEventOutcome = "skipped" | "delivered" | "failed";
|
|
65
|
+
|
|
66
|
+
export type EmitBackfillManifestEventParams = {
|
|
67
|
+
courseSlug: string;
|
|
68
|
+
runId: string;
|
|
69
|
+
result: GenerateCourseDescriptionsResult;
|
|
70
|
+
storage: CourseDescriptionsStorage;
|
|
71
|
+
rigobotToken: string;
|
|
72
|
+
breathecodeToken: string;
|
|
73
|
+
/** Injection points for tests. */
|
|
74
|
+
emit?: typeof sendPublishEvent;
|
|
75
|
+
readPackageInfo?: typeof fetchPackageInfo;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Announce one reconciled course. Never throws: the descriptions and the
|
|
80
|
+
* manifest are already written by the time this runs, and losing the
|
|
81
|
+
* notification must not turn a successful course into a failed one.
|
|
82
|
+
*/
|
|
83
|
+
export async function emitBackfillManifestEvent(
|
|
84
|
+
params: EmitBackfillManifestEventParams
|
|
85
|
+
): Promise<BackfillEventOutcome> {
|
|
86
|
+
const { courseSlug, result } = params
|
|
87
|
+
|
|
88
|
+
if (result.status === "skipped") {
|
|
89
|
+
return "skipped"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const emit = params.emit ?? sendPublishEvent
|
|
93
|
+
const readPackageInfo = params.readPackageInfo ?? fetchPackageInfo
|
|
94
|
+
|
|
95
|
+
// Enriching the payload must never prevent the delivery: both the manifest and
|
|
96
|
+
// the package info are optional in the contract, and an event with less
|
|
97
|
+
// context beats an event that never arrives.
|
|
98
|
+
let manifest: PackageManifest | null = null
|
|
99
|
+
try {
|
|
100
|
+
if (params.storage.readManifest) {
|
|
101
|
+
manifest = await params.storage.readManifest(courseSlug)
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(
|
|
105
|
+
`[backfill-events] Could not read the manifest of "${courseSlug}", reporting without it:`,
|
|
106
|
+
(error as Error).message
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let packageInfo: Record<string, unknown> | null = null
|
|
111
|
+
try {
|
|
112
|
+
packageInfo = await readPackageInfo(courseSlug, params.rigobotToken)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[backfill-events] Could not read the package "${courseSlug}", reporting without it:`,
|
|
116
|
+
(error as Error).message
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const delivered = await emit(
|
|
122
|
+
buildPackageManifestUpdatedEvent(
|
|
123
|
+
{
|
|
124
|
+
publishId: backfillPublishId(params.runId, courseSlug),
|
|
125
|
+
courseSlug,
|
|
126
|
+
packageInfo,
|
|
127
|
+
manifest,
|
|
128
|
+
},
|
|
129
|
+
// A failed projection does not fail the run — the syllabus is saved
|
|
130
|
+
// either way — but this event reports the manifest, so it cannot claim
|
|
131
|
+
// success over one that was never rewritten.
|
|
132
|
+
result.status === "failed" || result.manifestProjected === false ?
|
|
133
|
+
"failed" :
|
|
134
|
+
"success",
|
|
135
|
+
{
|
|
136
|
+
generated: result.generated,
|
|
137
|
+
failed: result.failed,
|
|
138
|
+
missing: result.missing,
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
params.breathecodeToken
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return delivered ? "delivered" : "failed"
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(
|
|
147
|
+
`[backfill-events] Could not deliver the manifest event for "${courseSlug}":`,
|
|
148
|
+
(error as Error).message
|
|
149
|
+
)
|
|
150
|
+
return "failed"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -79,6 +79,13 @@ export type GenerateCourseDescriptionsResult = {
|
|
|
79
79
|
errors: string[];
|
|
80
80
|
/** Completion durations, to watch how close we run to the request limits. */
|
|
81
81
|
durationsSeconds: number[];
|
|
82
|
+
/**
|
|
83
|
+
* Whether `package-manifest.json` was re-projected; null when the storage does
|
|
84
|
+
* not project at all. A failed projection is not a failed run — the syllabus,
|
|
85
|
+
* which is the source of truth, is already saved — but callers that announce
|
|
86
|
+
* the manifest downstream must not report success on it.
|
|
87
|
+
*/
|
|
88
|
+
manifestProjected: boolean | null;
|
|
82
89
|
};
|
|
83
90
|
|
|
84
91
|
function emptyResult(
|
|
@@ -92,6 +99,7 @@ function emptyResult(
|
|
|
92
99
|
missing: 0,
|
|
93
100
|
errors: [],
|
|
94
101
|
durationsSeconds: [],
|
|
102
|
+
manifestProjected: null,
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
|
|
@@ -288,9 +296,11 @@ export async function generateCourseDescriptions(
|
|
|
288
296
|
if (storage.reprojectManifest) {
|
|
289
297
|
try {
|
|
290
298
|
await storage.reprojectManifest(courseSlug)
|
|
299
|
+
result.manifestProjected = true
|
|
291
300
|
} catch (error) {
|
|
292
301
|
// The syllabus (the source of truth) is already saved; a failed
|
|
293
302
|
// projection is recoverable and must not lose the generated text.
|
|
303
|
+
result.manifestProjected = false
|
|
294
304
|
result.errors.push(
|
|
295
305
|
`manifest projection failed: ${(error as Error).message}`
|
|
296
306
|
)
|