@learnpack/learnpack 5.0.353 → 5.0.354
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/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/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,168 @@
|
|
|
1
|
+
import { Bucket } from "@google-cloud/storage"
|
|
2
|
+
import { PutObjectCommand } from "@aws-sdk/client-s3"
|
|
3
|
+
import {
|
|
4
|
+
AwsClient,
|
|
5
|
+
fetchJsonObject,
|
|
6
|
+
fetchTextObject,
|
|
7
|
+
listObjectKeys,
|
|
8
|
+
withRetry,
|
|
9
|
+
} from "../s3/packageManifestBackfill"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The two buckets a package lives in, behind one interface.
|
|
13
|
+
*
|
|
14
|
+
* A legacy package is repaired **twice, independently** — once against the
|
|
15
|
+
* published snapshot in S3 and once against the draft in GCS — never by copying
|
|
16
|
+
* one into the other. The two hold different content (the draft keeps moving
|
|
17
|
+
* after a publication) and the synthesized structure is a projection of that
|
|
18
|
+
* content: a syllabus derived from the snapshot could name steps the draft does
|
|
19
|
+
* not have. This is the same rule `mirrorDescriptions` enforces with its content
|
|
20
|
+
* hash; deriving from each bucket needs no guard at all.
|
|
21
|
+
*
|
|
22
|
+
* What crosses between them is only the descriptions, through
|
|
23
|
+
* `descriptionsS3Backfill --mirror-draft`, which is hash-guarded per step.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export type RepairTarget = "s3" | "gcs";
|
|
27
|
+
|
|
28
|
+
export const ALL_REPAIR_TARGETS: RepairTarget[] = ["s3", "gcs"]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Where each input lives. The two buckets do not agree on this, and the
|
|
32
|
+
* disagreement is load-bearing:
|
|
33
|
+
*
|
|
34
|
+
* - S3 reads `{slug}/config.json` first (`fetchPackageSources`), which is where
|
|
35
|
+
* the publish writes it into the zip.
|
|
36
|
+
* - GCS reads `courses/{slug}/.learn/config.json` — the publish route downloads
|
|
37
|
+
* that one before anything else, so a config written anywhere else in the
|
|
38
|
+
* draft would not make the package publishable.
|
|
39
|
+
*/
|
|
40
|
+
export type PackageLayout = {
|
|
41
|
+
learnJsonKey(slug: string): string;
|
|
42
|
+
/** Config locations in resolution order; the first is the write target. */
|
|
43
|
+
configKeys(slug: string): string[];
|
|
44
|
+
syllabusKey(slug: string): string;
|
|
45
|
+
/** Sidebar locations in resolution order; the first is the write target. */
|
|
46
|
+
sidebarKeys(slug: string): string[];
|
|
47
|
+
exercisesPrefix(slug: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Whether `config.json` is the authority on the exercise list, or the folder
|
|
50
|
+
* listing is.
|
|
51
|
+
*
|
|
52
|
+
* This decides the **language keys** of the synthesized syllabus, and the two
|
|
53
|
+
* buckets genuinely disagree:
|
|
54
|
+
*
|
|
55
|
+
* - S3 reads the published `config.json` (`fetchPackageSources`), where these
|
|
56
|
+
* legacy packages carry `us` for the unsuffixed README.
|
|
57
|
+
* - GCS has no such reader. Both the publish-time manifest and the
|
|
58
|
+
* descriptions pipeline go through `configBuilder.buildConfig`, which
|
|
59
|
+
* derives exercises from the listing and maps an unsuffixed `README.md` to
|
|
60
|
+
* `en` — regardless of what the draft's `.learn/config.json` says.
|
|
61
|
+
*
|
|
62
|
+
* A syllabus keyed the other way is not read at all: the manifest would look
|
|
63
|
+
* up `translations["en"]`, find nothing, and publish `description: null` for
|
|
64
|
+
* every step. So each side is built the way its own reader will read it.
|
|
65
|
+
*/
|
|
66
|
+
exercisesFromConfig: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type RepairStorage = {
|
|
70
|
+
target: RepairTarget;
|
|
71
|
+
layout: PackageLayout;
|
|
72
|
+
readJson<T>(key: string): Promise<T | null>;
|
|
73
|
+
readText(key: string): Promise<string | null>;
|
|
74
|
+
listKeys(prefix: string): Promise<string[]>;
|
|
75
|
+
writeJson(key: string, value: unknown): Promise<void>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const s3Layout: PackageLayout = {
|
|
79
|
+
learnJsonKey: slug => `${slug}/learn.json`,
|
|
80
|
+
configKeys: slug => [`${slug}/config.json`, `${slug}/.learn/config.json`],
|
|
81
|
+
syllabusKey: slug => `${slug}/.learn/initialSyllabus.json`,
|
|
82
|
+
// 25 of the published packages keep their sidebar at the root. It has to be
|
|
83
|
+
// resolved before writing: `fetchPackageSources` prefers the `.learn/` copy,
|
|
84
|
+
// so a synthesized one would shadow the published titles.
|
|
85
|
+
sidebarKeys: slug => [
|
|
86
|
+
`${slug}/.learn/sidebar.json`,
|
|
87
|
+
`${slug}/sidebar.json`,
|
|
88
|
+
],
|
|
89
|
+
exercisesPrefix: slug => `${slug}/exercises/`,
|
|
90
|
+
exercisesFromConfig: true,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const gcsLayout: PackageLayout = {
|
|
94
|
+
learnJsonKey: slug => `courses/${slug}/learn.json`,
|
|
95
|
+
configKeys: slug => [
|
|
96
|
+
`courses/${slug}/.learn/config.json`,
|
|
97
|
+
`courses/${slug}/config.json`,
|
|
98
|
+
],
|
|
99
|
+
syllabusKey: slug => `courses/${slug}/.learn/initialSyllabus.json`,
|
|
100
|
+
sidebarKeys: slug => [`courses/${slug}/.learn/sidebar.json`],
|
|
101
|
+
exercisesPrefix: slug => `courses/${slug}/exercises/`,
|
|
102
|
+
exercisesFromConfig: false,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createS3RepairStorage(
|
|
106
|
+
s3: AwsClient,
|
|
107
|
+
bucket: string
|
|
108
|
+
): RepairStorage {
|
|
109
|
+
return {
|
|
110
|
+
target: "s3",
|
|
111
|
+
layout: s3Layout,
|
|
112
|
+
readJson: key => fetchJsonObject(s3, bucket, key),
|
|
113
|
+
readText: key => fetchTextObject(s3, bucket, key),
|
|
114
|
+
listKeys: prefix => listObjectKeys(s3, bucket, prefix),
|
|
115
|
+
async writeJson(key, value) {
|
|
116
|
+
await withRetry(() =>
|
|
117
|
+
s3.send(
|
|
118
|
+
new PutObjectCommand({
|
|
119
|
+
Bucket: bucket,
|
|
120
|
+
Key: key,
|
|
121
|
+
Body: JSON.stringify(value, null, 2),
|
|
122
|
+
ContentType: "application/json",
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createGcsRepairStorage(bucket: Bucket): RepairStorage {
|
|
131
|
+
const download = async (key: string): Promise<string | null> => {
|
|
132
|
+
try {
|
|
133
|
+
const [buf] = await bucket.file(key).download()
|
|
134
|
+
return buf.toString()
|
|
135
|
+
} catch {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
target: "gcs",
|
|
142
|
+
layout: gcsLayout,
|
|
143
|
+
async readJson<T>(key: string): Promise<T | null> {
|
|
144
|
+
const content = await download(key)
|
|
145
|
+
if (content === null) {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(content) as T
|
|
151
|
+
} catch {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
readText: download,
|
|
156
|
+
async listKeys(prefix) {
|
|
157
|
+
const [files] = await bucket.getFiles({ prefix })
|
|
158
|
+
return files.map(file => file.name)
|
|
159
|
+
},
|
|
160
|
+
async writeJson(key, value) {
|
|
161
|
+
await bucket
|
|
162
|
+
.file(key)
|
|
163
|
+
.save(Buffer.from(JSON.stringify(value, null, 2), "utf8"), {
|
|
164
|
+
contentType: "application/json",
|
|
165
|
+
})
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|