@sous-io/sous 0.2.37 → 1.0.2
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/README.md +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1094 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -245
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -410
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -413
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1107
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
|
@@ -1,1108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regenerating a repository's `sous.index.json` from what it actually publishes.
|
|
3
|
-
*
|
|
4
|
-
* The index is the portable contract every provider hands back, so it must
|
|
5
|
-
* describe published reality and nothing else. Three rules follow from that,
|
|
6
|
-
* and this module exists to keep them:
|
|
7
|
-
*
|
|
8
|
-
* 1. A published version is IMMUTABLE. An entry that is already in the index
|
|
9
|
-
* and carries a tag keeps the hash and the dependencies it was published
|
|
10
|
-
* with; a recomputed hash that disagrees, or dependencies that no longer
|
|
11
|
-
* agree with what the version's manifest declares, are reported as errors,
|
|
12
|
-
* never quietly written over.
|
|
13
|
-
* 2. A version is published when a tag carries it, and every version the index
|
|
14
|
-
* records has to have one. The single exception is the version a recipe's
|
|
15
|
-
* manifest declares right now: a release regenerates the index, commits it
|
|
16
|
-
* alongside the manifests, and tags that commit, so for the length of that
|
|
17
|
-
* one commit the index describes a version whose tag is about to exist. Any
|
|
18
|
-
* OLDER version without a tag is an error, because that is a published
|
|
19
|
-
* version nothing can fetch.
|
|
20
|
-
* 3. The tags are the backstop. A version that has a tag but is missing from the
|
|
21
|
-
* index is rebuilt from the tagged tree, dependencies included, so deleting
|
|
22
|
-
* the index file and regenerating it restores the same catalog.
|
|
23
|
-
*
|
|
24
|
-
* Each version entry also records what it was released against, so a consumer
|
|
25
|
-
* installs the versions a recipe was published with rather than re-resolving
|
|
26
|
-
* its ranges long afterwards. Those dependencies are resolved exactly once: when
|
|
27
|
-
* the version is first recorded, against the repository as it stands then (or,
|
|
28
|
-
* for a version rebuilt from its tag, as it stood at that tag), and never again.
|
|
29
|
-
*
|
|
30
|
-
* A version recorded here is also DESCRIBED, so a consumer can say what
|
|
31
|
-
* subscribing to it brings in and asks before anything is fetched: each
|
|
32
|
-
* dependency carries the manifest entry that declared it and whether it is a
|
|
33
|
-
* co-subscription or a build dependency, and the version carries the variable
|
|
34
|
-
* definitions its manifest publishes. Those fields are frozen with the rest of
|
|
35
|
-
* the entry; a version published before they existed keeps its entry as it is.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import fs from "node:fs";
|
|
39
|
-
import path from "node:path";
|
|
40
|
-
import semver from "semver";
|
|
41
|
-
import { INDEX_FILENAME } from "../formats/common.js";
|
|
42
|
-
import {
|
|
43
|
-
parseIndexFile,
|
|
44
|
-
stringifyIndexFile,
|
|
45
|
-
type IndexDependency,
|
|
46
|
-
type IndexFile,
|
|
47
|
-
type IndexRecipe,
|
|
48
|
-
type IndexVersion,
|
|
49
|
-
} from "../formats/index-file.js";
|
|
50
|
-
import { parseRecipeManifest } from "../formats/recipe-manifest.js";
|
|
51
|
-
import { loadJsonFile, parseYamlText, parseJsoncText } from "../load-manifest.js";
|
|
52
|
-
import { hashDirectory } from "../store/hash.js";
|
|
53
|
-
import type { RunOptions } from "../providers/git.js";
|
|
54
|
-
import {
|
|
55
|
-
listRecipeTags,
|
|
56
|
-
readFileAtTag,
|
|
57
|
-
recipeTagKey,
|
|
58
|
-
tagCommitDate,
|
|
59
|
-
tagFor,
|
|
60
|
-
withTaggedTree,
|
|
61
|
-
type RecipeTag,
|
|
62
|
-
} from "./tags.js";
|
|
63
|
-
import {
|
|
64
|
-
describeError,
|
|
65
|
-
validateRepo,
|
|
66
|
-
type RepoValidation,
|
|
67
|
-
type ValidatedRecipe,
|
|
68
|
-
type ValidationProblem,
|
|
69
|
-
} from "./validate.js";
|
|
70
|
-
import {
|
|
71
|
-
describeDependencyKind,
|
|
72
|
-
foldDeclaration,
|
|
73
|
-
type CoveringEntry,
|
|
74
|
-
type DependencyDeclaration,
|
|
75
|
-
type GatheredDeclaration,
|
|
76
|
-
} from "../declarations.js";
|
|
77
|
-
import type { DependencyKind } from "../formats/common.js";
|
|
78
|
-
import {
|
|
79
|
-
RefSource,
|
|
80
|
-
isNamedRef,
|
|
81
|
-
locationOf,
|
|
82
|
-
namespaceOfKey,
|
|
83
|
-
rangeOf,
|
|
84
|
-
sharedRefResolver,
|
|
85
|
-
shortKey,
|
|
86
|
-
} from "../../../services/ref-resolver/index.js";
|
|
87
|
-
import type { SettledDependency } from "./settle.js";
|
|
88
|
-
|
|
89
|
-
/** A version that is ready to publish but has no tag yet. */
|
|
90
|
-
export type PendingRelease = {
|
|
91
|
-
/** The recipe's key, `namespace/recipe`. */
|
|
92
|
-
key: string;
|
|
93
|
-
/** The recipe folder, relative to the repository root. */
|
|
94
|
-
path: string;
|
|
95
|
-
/** The version its manifest declares. */
|
|
96
|
-
version: string;
|
|
97
|
-
/** The tag that would carry it. */
|
|
98
|
-
tag: string;
|
|
99
|
-
/** The content hash of the recipe folder as it stands in the working tree. */
|
|
100
|
-
hash: string;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
/** What one regeneration produced. */
|
|
104
|
-
export type IndexBuildResult = {
|
|
105
|
-
/** The regenerated index, ready to be written. */
|
|
106
|
-
index: IndexFile;
|
|
107
|
-
/** The index exactly as it would be written, so a caller can compare or save it. */
|
|
108
|
-
text: string;
|
|
109
|
-
/** True when the committed index differs from the regenerated one. */
|
|
110
|
-
stale: boolean;
|
|
111
|
-
/** Versions with no tag yet, in the order the repo manifest lists their recipes. */
|
|
112
|
-
pending: PendingRelease[];
|
|
113
|
-
/** Everything the regeneration found wrong, errors and warnings together. */
|
|
114
|
-
problems: ValidationProblem[];
|
|
115
|
-
/** Every recipe release tag the repository carries. */
|
|
116
|
-
tags: RecipeTag[];
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
/** What `buildIndex` needs to know. */
|
|
120
|
-
export type BuildIndexOptions = {
|
|
121
|
-
/** The validated repository, from `validateRepo`. */
|
|
122
|
-
validation: RepoValidation;
|
|
123
|
-
/** The committed index, when there is a readable one. */
|
|
124
|
-
existing?: IndexFile;
|
|
125
|
-
/** The version of sous doing the regeneration, recorded as the generator. */
|
|
126
|
-
sousVersion: string;
|
|
127
|
-
/** When the regeneration ran. Defaults to now. */
|
|
128
|
-
now?: Date;
|
|
129
|
-
/** Every release tag, when the caller has already listed them. */
|
|
130
|
-
tags?: RecipeTag[];
|
|
131
|
-
/** The command runner git calls go through. */
|
|
132
|
-
run?: RunOptions["run"];
|
|
133
|
-
/**
|
|
134
|
-
* The versions this run is about to publish, keyed `namespace/recipe`. Their
|
|
135
|
-
* tags do not exist yet: the index is regenerated, committed alongside the
|
|
136
|
-
* manifests, and only then tagged, so the tagged commit already carries an
|
|
137
|
-
* index that describes itself.
|
|
138
|
-
*/
|
|
139
|
-
publishing?: Record<string, string>;
|
|
140
|
-
/**
|
|
141
|
-
* The dependencies that read more than one way, as `settleDependencyLocations`
|
|
142
|
-
* settled them, keyed by the dependency as written. Each is recorded under
|
|
143
|
-
* the keys it reached, with the repository it settled on.
|
|
144
|
-
*/
|
|
145
|
-
settled?: Map<string, SettledDependency>;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
/** The index file's absolute path in a repository. */
|
|
149
|
-
export function indexFilePath(rootDir: string): string {
|
|
150
|
-
return path.join(rootDir, INDEX_FILENAME);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Reads and validates the committed index, or returns undefined when there is
|
|
155
|
-
* none. A file that is present but unreadable raises a ConfigError; the caller
|
|
156
|
-
* decides whether to fail or to regenerate from the tags instead.
|
|
157
|
-
*
|
|
158
|
-
* @param rootDir - The repository's root directory.
|
|
159
|
-
*/
|
|
160
|
-
export function readIndexFile(rootDir: string): IndexFile | undefined {
|
|
161
|
-
const filePath = indexFilePath(rootDir);
|
|
162
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
163
|
-
return parseIndexFile(loadJsonFile(filePath, "repository index"), filePath);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Regenerates a repository's index from its recipe manifests and its tags.
|
|
168
|
-
*
|
|
169
|
-
* @param options - The validated repository and everything the regeneration needs.
|
|
170
|
-
*/
|
|
171
|
-
export async function buildIndex(
|
|
172
|
-
options: BuildIndexOptions
|
|
173
|
-
): Promise<IndexBuildResult> {
|
|
174
|
-
const { validation, existing, sousVersion } = options;
|
|
175
|
-
const rootDir = validation.rootDir;
|
|
176
|
-
const now = options.now ?? new Date();
|
|
177
|
-
const run = options.run;
|
|
178
|
-
|
|
179
|
-
const tags = options.tags ?? (await listRecipeTags(rootDir, { run }));
|
|
180
|
-
const tagsByKey = groupTagsByKey(tags);
|
|
181
|
-
const problems: ValidationProblem[] = [];
|
|
182
|
-
const pending: PendingRelease[] = [];
|
|
183
|
-
|
|
184
|
-
const namespaces: IndexFile["namespaces"] = {};
|
|
185
|
-
for (const [name, declaration] of Object.entries(validation.manifest.namespaces)) {
|
|
186
|
-
namespaces[name] =
|
|
187
|
-
declaration.description === undefined
|
|
188
|
-
? {}
|
|
189
|
-
: { description: declaration.description };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const recipes: IndexFile["recipes"] = {};
|
|
193
|
-
|
|
194
|
-
// What each recipe in this repository is published at once this run is done,
|
|
195
|
-
// and every version each of them publishes. Both are needed to turn a
|
|
196
|
-
// dependency's declared range into the exact version a consumer installs.
|
|
197
|
-
const publishing = options.publishing ?? {};
|
|
198
|
-
const current = siblingState(validation, tagsByKey, existing, publishing);
|
|
199
|
-
|
|
200
|
-
for (const recipe of validation.recipes) {
|
|
201
|
-
if (!Object.hasOwn(namespaces, recipe.manifest.namespace)) continue;
|
|
202
|
-
|
|
203
|
-
const key = recipe.key;
|
|
204
|
-
const existingVersions = existing?.recipes[key]?.versions ?? {};
|
|
205
|
-
const versions: Record<string, IndexVersion> = { ...existingVersions };
|
|
206
|
-
const recipeTags = tagsByKey.get(key) ?? [];
|
|
207
|
-
const manifestName = path.basename(recipe.manifestPath);
|
|
208
|
-
|
|
209
|
-
// Rule 3: every tagged version belongs in the index, including ones the
|
|
210
|
-
// committed index has lost. Its dependencies are resolved against the
|
|
211
|
-
// repository as it stood at the tag, which is what it was released against.
|
|
212
|
-
for (const tag of recipeTags) {
|
|
213
|
-
if (Object.hasOwn(versions, tag.version)) continue;
|
|
214
|
-
const rebuilt = await rebuildTaggedVersion(
|
|
215
|
-
rootDir,
|
|
216
|
-
tag,
|
|
217
|
-
recipe,
|
|
218
|
-
tagsByKey,
|
|
219
|
-
existing,
|
|
220
|
-
run,
|
|
221
|
-
options.settled
|
|
222
|
-
);
|
|
223
|
-
problems.push(...rebuilt.problems);
|
|
224
|
-
versions[tag.version] = {
|
|
225
|
-
hash: rebuilt.hash,
|
|
226
|
-
tag: tag.tag,
|
|
227
|
-
prerelease: semver.prerelease(tag.version) !== null,
|
|
228
|
-
...(await releasedAtOf(rootDir, tag.tag, run)),
|
|
229
|
-
...rebuilt.description,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const version = recipe.manifest.version;
|
|
234
|
-
const tagName = tagFor(recipe.manifest.namespace, recipe.manifest.name, version);
|
|
235
|
-
const hasTag = recipeTags.some((entry) => entry.tag === tagName);
|
|
236
|
-
const workingHash = await hashDirectory(recipe.dir);
|
|
237
|
-
const where = relativeTo(rootDir, recipe.manifestPath);
|
|
238
|
-
const declared = declaredDependencies(recipe, validation, options.settled);
|
|
239
|
-
|
|
240
|
-
// A version the index publishes must have a tag, with one exception: the
|
|
241
|
-
// version the manifest declares right now, which is the one a release is in
|
|
242
|
-
// the middle of publishing. Everything older is history, and history with a
|
|
243
|
-
// missing tag is a published version nothing can fetch.
|
|
244
|
-
for (const [known, published] of Object.entries(existingVersions)) {
|
|
245
|
-
if (known === version) continue;
|
|
246
|
-
if (recipeTags.some((entry) => entry.version === known)) continue;
|
|
247
|
-
problems.push({
|
|
248
|
-
level: "error",
|
|
249
|
-
where: `${INDEX_FILENAME} recipes['${key}'].versions['${known}']`,
|
|
250
|
-
message:
|
|
251
|
-
`the index publishes version ${known}, but the tag '${published.tag}' does not ` +
|
|
252
|
-
`exist in this repository. A missing tag must never hide a published version; ` +
|
|
253
|
-
`restore the tag, or remove the version from the index.`,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (hasTag) {
|
|
258
|
-
problems.push(
|
|
259
|
-
...(await checkTaggedMetadata(rootDir, tagName, recipe.path, manifestName, version, where, run))
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const taggedHash = await hashTaggedRecipe(rootDir, tagName, recipe.path, run);
|
|
263
|
-
const published = existingVersions[version];
|
|
264
|
-
|
|
265
|
-
if (published !== undefined && published.hash !== taggedHash) {
|
|
266
|
-
problems.push({
|
|
267
|
-
level: "error",
|
|
268
|
-
where: `${INDEX_FILENAME} recipes['${key}'].versions['${version}']`,
|
|
269
|
-
message:
|
|
270
|
-
`the index records a different content hash from the one the tag '${tagName}' ` +
|
|
271
|
-
`now carries. A published version never changes, so either the tag was moved ` +
|
|
272
|
-
`or the index was edited; restore whichever is wrong before releasing again.`,
|
|
273
|
-
});
|
|
274
|
-
} else if (published !== undefined) {
|
|
275
|
-
// Already published: the entry is carried forward as it stands, its
|
|
276
|
-
// dependencies and any field a later sous wrote included. Only a missing
|
|
277
|
-
// release date is filled in, from the tag.
|
|
278
|
-
if (published.releasedAt === undefined) {
|
|
279
|
-
versions[version] = { ...published, ...(await releasedAtOf(rootDir, tagName, run)) };
|
|
280
|
-
}
|
|
281
|
-
// The check needs the manifest that was published, which the working one
|
|
282
|
-
// is only while the folder still matches the tag. An entry with no
|
|
283
|
-
// dependencies at all was published before sous recorded them; a
|
|
284
|
-
// consumer resolves that version's ranges, and it stays that way.
|
|
285
|
-
if (taggedHash === workingHash && published.dependencies !== undefined) {
|
|
286
|
-
problems.push(
|
|
287
|
-
...checkRecordedDependencies(key, version, published.dependencies, declared)
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
if (taggedHash === workingHash) {
|
|
291
|
-
problems.push(...checkRecordedLists(key, version, published, recipe));
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// A tagged version the index did not record was rebuilt from its tag above.
|
|
295
|
-
|
|
296
|
-
if (taggedHash !== workingHash) {
|
|
297
|
-
// A recipe this run is not publishing is allowed to have unpublished
|
|
298
|
-
// changes sitting in it: that is what a scoped release leaves behind,
|
|
299
|
-
// and it is worth saying rather than stopping for.
|
|
300
|
-
const publishingThis =
|
|
301
|
-
options.publishing === undefined || Object.hasOwn(publishing, key);
|
|
302
|
-
problems.push({
|
|
303
|
-
level: publishingThis ? "error" : "warning",
|
|
304
|
-
where,
|
|
305
|
-
message: publishingThis
|
|
306
|
-
? `version ${version} was already published as the tag '${tagName}', and the ` +
|
|
307
|
-
`files in this folder no longer match what that tag carries. A published ` +
|
|
308
|
-
`version never changes: bump the version in this manifest, or with ` +
|
|
309
|
-
`'sous repo release --bump patch'.`
|
|
310
|
-
: `version ${version} is published as the tag '${tagName}', and this folder has ` +
|
|
311
|
-
`changed since. Those changes are outside this release, so they stay ` +
|
|
312
|
-
`unpublished; release '${key}' to publish them.`,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
// No tag yet. The version is either one this run is about to cut, or one
|
|
317
|
-
// a release commit already recorded and CI will cut after the merge; in
|
|
318
|
-
// both cases the index describes it from the working tree, and the tag
|
|
319
|
-
// follows on the very commit that carries this index.
|
|
320
|
-
const published = existingVersions[version];
|
|
321
|
-
if (options.publishing?.[key] === version || published !== undefined) {
|
|
322
|
-
versions[version] = {
|
|
323
|
-
hash: workingHash,
|
|
324
|
-
tag: tagName,
|
|
325
|
-
prerelease: semver.prerelease(version) !== null,
|
|
326
|
-
releasedAt: published?.releasedAt ?? now.toISOString(),
|
|
327
|
-
...describeVersion(recipe, declared, current),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
pending.push({
|
|
332
|
-
key,
|
|
333
|
-
path: recipe.path,
|
|
334
|
-
version,
|
|
335
|
-
tag: tagName,
|
|
336
|
-
hash: workingHash,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (Object.keys(versions).length > 0) {
|
|
341
|
-
const entry: IndexRecipe = { path: recipe.path, versions };
|
|
342
|
-
if (recipe.manifest.description !== undefined) {
|
|
343
|
-
entry.description = recipe.manifest.description;
|
|
344
|
-
}
|
|
345
|
-
recipes[key] = entry;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
problems.push(...checkOrphanTags(validation, tags));
|
|
350
|
-
|
|
351
|
-
const index: IndexFile = {
|
|
352
|
-
formatVersion: 1,
|
|
353
|
-
name: validation.manifest.name,
|
|
354
|
-
generatedAt: now.toISOString(),
|
|
355
|
-
generator: sousVersion,
|
|
356
|
-
namespaces,
|
|
357
|
-
recipes,
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// The stamp changes on every run, so it is only allowed to change the file
|
|
361
|
-
// when something else did too; otherwise `--check` would call a perfectly
|
|
362
|
-
// current index stale every time it ran.
|
|
363
|
-
if (existing !== undefined && sameExceptStamp(existing, index)) {
|
|
364
|
-
index.generatedAt = existing.generatedAt;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const text = stringifyIndexFile(index);
|
|
368
|
-
const stale = existing === undefined || stringifyIndexFile(existing) !== text;
|
|
369
|
-
|
|
370
|
-
return { index, text, stale, pending, problems, tags };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Says, in plain language, how a committed index differs from a regenerated
|
|
375
|
-
* one. This is what `--check` prints when it refuses: an author needs to know
|
|
376
|
-
* which recipe and which version is out of step, not that two files differ.
|
|
377
|
-
*
|
|
378
|
-
* @param existing - The committed index, or undefined when there is none.
|
|
379
|
-
* @param rebuilt - The regenerated index.
|
|
380
|
-
*/
|
|
381
|
-
export function describeIndexDrift(
|
|
382
|
-
existing: IndexFile | undefined,
|
|
383
|
-
rebuilt: IndexFile
|
|
384
|
-
): string[] {
|
|
385
|
-
if (existing === undefined) {
|
|
386
|
-
return [`there is no ${INDEX_FILENAME} in this repository yet`];
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const lines: string[] = [];
|
|
390
|
-
|
|
391
|
-
if (existing.name !== rebuilt.name) {
|
|
392
|
-
lines.push(
|
|
393
|
-
`the index calls this repository '${existing.name}', and its manifest calls it ` +
|
|
394
|
-
`'${rebuilt.name}'`
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
if (existing.generator !== rebuilt.generator) {
|
|
398
|
-
lines.push(
|
|
399
|
-
`the index was generated by sous ${existing.generator}, and this is sous ` +
|
|
400
|
-
`${rebuilt.generator}`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
for (const name of Object.keys(rebuilt.namespaces)) {
|
|
405
|
-
if (!Object.hasOwn(existing.namespaces, name)) {
|
|
406
|
-
lines.push(`the namespace '${name}' is missing from the index`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
for (const name of Object.keys(existing.namespaces)) {
|
|
410
|
-
if (!Object.hasOwn(rebuilt.namespaces, name)) {
|
|
411
|
-
lines.push(`the index still declares the namespace '${name}'`);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
for (const [key, recipe] of Object.entries(rebuilt.recipes)) {
|
|
416
|
-
const before = existing.recipes[key];
|
|
417
|
-
if (before === undefined) {
|
|
418
|
-
lines.push(`the recipe '${key}' is missing from the index`);
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (before.path !== recipe.path) {
|
|
422
|
-
lines.push(
|
|
423
|
-
`the recipe '${key}' is listed at '${before.path}' in the index and at ` +
|
|
424
|
-
`'${recipe.path}' in the repository`
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
if (before.description !== recipe.description) {
|
|
428
|
-
lines.push(`the description of '${key}' has changed since the index was written`);
|
|
429
|
-
}
|
|
430
|
-
for (const version of Object.keys(recipe.versions)) {
|
|
431
|
-
if (!Object.hasOwn(before.versions, version)) {
|
|
432
|
-
lines.push(`version ${version} of '${key}' is missing from the index`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
for (const [key, recipe] of Object.entries(existing.recipes)) {
|
|
438
|
-
if (!Object.hasOwn(rebuilt.recipes, key)) {
|
|
439
|
-
lines.push(`the index still publishes '${key}', which this repository does not`);
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
for (const version of Object.keys(recipe.versions)) {
|
|
443
|
-
if (!Object.hasOwn(rebuilt.recipes[key]!.versions, version)) {
|
|
444
|
-
lines.push(`the index still publishes version ${version} of '${key}'`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (lines.length === 0) {
|
|
450
|
-
lines.push("the index differs from what this repository describes");
|
|
451
|
-
}
|
|
452
|
-
return lines;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// --- Internals ----------------------------------------------------------------------------------
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* One dependency a recipe's manifest declares, keyed `namespace/recipe` in
|
|
459
|
-
* `DeclaredDependencies`. A whole-namespace declaration is expanded into one
|
|
460
|
-
* entry per recipe the namespace holds.
|
|
461
|
-
*/
|
|
462
|
-
type DeclaredLocation =
|
|
463
|
-
| {
|
|
464
|
-
/** A recipe in another repository, named by its location. */
|
|
465
|
-
where: "remote";
|
|
466
|
-
/** The canonical identity of the repository publishing it. */
|
|
467
|
-
repo: string;
|
|
468
|
-
/** The range the manifest declared, `*` when it declared none. */
|
|
469
|
-
range: string;
|
|
470
|
-
}
|
|
471
|
-
| {
|
|
472
|
-
/** A recipe in this same repository. */
|
|
473
|
-
where: "sibling";
|
|
474
|
-
/** The range the manifest declared, when it declared one. */
|
|
475
|
-
range?: string;
|
|
476
|
-
/**
|
|
477
|
-
* True when the manifest names this recipe itself, rather than only the
|
|
478
|
-
* namespace holding it. A recipe a namespace gained after a version was
|
|
479
|
-
* published is rightly missing from that version's entry; one the
|
|
480
|
-
* manifest names is not.
|
|
481
|
-
*/
|
|
482
|
-
named: boolean;
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
/** One declared dependency: where it lives, and the manifest entry bringing it in. */
|
|
486
|
-
type DeclaredDependency = DeclaredLocation & DependencyDeclaration;
|
|
487
|
-
|
|
488
|
-
/** Everything one recipe's manifest declares, under `depends` and `subscribes`. */
|
|
489
|
-
type DeclaredDependencies = {
|
|
490
|
-
/** Every dependency, keyed `namespace/recipe`; a later declaration wins over an earlier one. */
|
|
491
|
-
byKey: Map<string, DeclaredDependency>;
|
|
492
|
-
/** The namespaces of this repository the manifest declares whole. */
|
|
493
|
-
namespaces: Set<string>;
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
/** What each recipe of a repository is published at, as one resolution sees it. */
|
|
497
|
-
type SiblingState = {
|
|
498
|
-
/** The version each recipe is released at, which is what "released alongside me" means. */
|
|
499
|
-
settled: Map<string, string>;
|
|
500
|
-
/** Every version each recipe publishes, for resolving a declared range. */
|
|
501
|
-
published: Map<string, string[]>;
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Reads the dependencies a recipe's manifest declares, expanding a
|
|
506
|
-
* whole-namespace declaration into the recipes that namespace holds.
|
|
507
|
-
*
|
|
508
|
-
* @param recipe - The recipe whose dependencies are being read.
|
|
509
|
-
* @param validation - The validated repository, for expanding namespace refs.
|
|
510
|
-
* @param settled - The dependencies that read more than one way, as the release settled them.
|
|
511
|
-
*/
|
|
512
|
-
function declaredDependencies(
|
|
513
|
-
recipe: ValidatedRecipe,
|
|
514
|
-
validation: RepoValidation,
|
|
515
|
-
settled: Map<string, SettledDependency> = new Map()
|
|
516
|
-
): DeclaredDependencies {
|
|
517
|
-
const byKey = new Map<string, DeclaredLocation>();
|
|
518
|
-
const namespaces = new Set<string>();
|
|
519
|
-
// Which entry brings each recipe in, and whether as a co-subscription, by the
|
|
520
|
-
// rule every reader of the index shares (`foldDeclaration`).
|
|
521
|
-
const declarations = new Map<string, GatheredDeclaration>();
|
|
522
|
-
const declared: ReadonlyArray<readonly [DependencyKind, string]> = [
|
|
523
|
-
...(recipe.manifest.depends ?? []).map((written) => ["depends", written] as const),
|
|
524
|
-
...(recipe.manifest.subscribes ?? []).map((written) => ["subscribes", written] as const),
|
|
525
|
-
];
|
|
526
|
-
|
|
527
|
-
/** Records that one entry covers one recipe. */
|
|
528
|
-
const cover = (key: string, entry: CoveringEntry): void => {
|
|
529
|
-
declarations.set(key, foldDeclaration(declarations.get(key), entry));
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
/** Records one sibling, remembering whether any declaration named it. */
|
|
533
|
-
const addSibling = (key: string, range: string | undefined, named: boolean): void => {
|
|
534
|
-
const before = byKey.get(key);
|
|
535
|
-
const namedBefore = before?.where === "sibling" && before.named;
|
|
536
|
-
byKey.set(key, {
|
|
537
|
-
where: "sibling",
|
|
538
|
-
...(range === undefined ? {} : { range }),
|
|
539
|
-
named: named || namedBefore,
|
|
540
|
-
});
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
for (const [kind, written] of declared) {
|
|
544
|
-
// A dependency that reads more than one way was settled by the release,
|
|
545
|
-
// and what it settled on is what a consumer reads instead of probing.
|
|
546
|
-
const answer = settled.get(written.trim());
|
|
547
|
-
if (answer !== undefined) {
|
|
548
|
-
for (const key of answer.keys) {
|
|
549
|
-
byKey.set(key, { where: "remote", repo: answer.identity, range: answer.range ?? "*" });
|
|
550
|
-
cover(key, { written, kind, named: answer.keys.length === 1 });
|
|
551
|
-
}
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
let readings;
|
|
556
|
-
try {
|
|
557
|
-
readings = sharedRefResolver().parse(written, RefSource.Manifest).refs;
|
|
558
|
-
} catch {
|
|
559
|
-
// A dependency that does not parse is already reported by validation.
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
// Several readings, or a browser path, are settled above or reported by
|
|
563
|
-
// the settling step; there is nothing more to record here.
|
|
564
|
-
const parsed = readings[0]!;
|
|
565
|
-
if (readings.length !== 1 || !isNamedRef(parsed)) continue;
|
|
566
|
-
|
|
567
|
-
const location = locationOf(parsed);
|
|
568
|
-
if (location !== undefined) {
|
|
569
|
-
// A whole namespace in another repository is read from that repository's
|
|
570
|
-
// own index by the consumer; only a recipe has a key to record.
|
|
571
|
-
if (parsed.kind === "recipe") {
|
|
572
|
-
byKey.set(shortKey(parsed), {
|
|
573
|
-
where: "remote",
|
|
574
|
-
repo: location.identity,
|
|
575
|
-
range: rangeOf(parsed) ?? "*",
|
|
576
|
-
});
|
|
577
|
-
cover(shortKey(parsed), { written, kind, named: true });
|
|
578
|
-
}
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (parsed.kind === "recipe") {
|
|
583
|
-
addSibling(shortKey(parsed), rangeOf(parsed), true);
|
|
584
|
-
cover(shortKey(parsed), { written, kind, named: true });
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// A whole-namespace dependency means every recipe in that namespace.
|
|
589
|
-
namespaces.add(parsed.name);
|
|
590
|
-
for (const entry of validation.recipes) {
|
|
591
|
-
if (entry.manifest.namespace !== parsed.name) continue;
|
|
592
|
-
if (entry.key === recipe.key) continue;
|
|
593
|
-
addSibling(entry.key, undefined, false);
|
|
594
|
-
cover(entry.key, { written, kind, named: false });
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const described = new Map<string, DeclaredDependency>();
|
|
599
|
-
for (const [key, dependency] of byKey) {
|
|
600
|
-
const { declared: entry, kind } = declarations.get(key)!;
|
|
601
|
-
described.set(key, { ...dependency, declared: entry, kind });
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
return { byKey: described, namespaces };
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Works out what each recipe of a repository is published at, for resolving
|
|
609
|
-
* the dependencies of a version being recorded.
|
|
610
|
-
*
|
|
611
|
-
* Two moments call this. A run recording a NEW version resolves against the
|
|
612
|
-
* repository as it stands, counting the versions the run itself is publishing.
|
|
613
|
-
* A run rebuilding a version from its TAG resolves against the repository as
|
|
614
|
-
* it stood at that tag (no `publishing` given): a sibling is settled on the
|
|
615
|
-
* version its manifest declared there, and only versions up to that one count
|
|
616
|
-
* as published.
|
|
617
|
-
*
|
|
618
|
-
* @param validation - The repository, as it stands or as it stood at a tag.
|
|
619
|
-
* @param tagsByKey - Every release tag, grouped by recipe.
|
|
620
|
-
* @param existing - The committed index, when there is one.
|
|
621
|
-
* @param publishing - The versions this run publishes; omitted for a tag.
|
|
622
|
-
*/
|
|
623
|
-
function siblingState(
|
|
624
|
-
validation: RepoValidation,
|
|
625
|
-
tagsByKey: Map<string, RecipeTag[]>,
|
|
626
|
-
existing: IndexFile | undefined,
|
|
627
|
-
publishing?: Record<string, string>
|
|
628
|
-
): SiblingState {
|
|
629
|
-
const settled = new Map<string, string>();
|
|
630
|
-
const published = new Map<string, string[]>();
|
|
631
|
-
|
|
632
|
-
for (const recipe of validation.recipes) {
|
|
633
|
-
const version = publishing?.[recipe.key] ?? recipe.manifest.version;
|
|
634
|
-
settled.set(recipe.key, version);
|
|
635
|
-
|
|
636
|
-
const known = new Set((tagsByKey.get(recipe.key) ?? []).map((tag) => tag.version));
|
|
637
|
-
for (const other of Object.keys(existing?.recipes[recipe.key]?.versions ?? {})) {
|
|
638
|
-
known.add(other);
|
|
639
|
-
}
|
|
640
|
-
known.add(version);
|
|
641
|
-
|
|
642
|
-
const all = [...known];
|
|
643
|
-
published.set(
|
|
644
|
-
recipe.key,
|
|
645
|
-
publishing === undefined ? all.filter((other) => semver.lte(other, version)) : all
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return { settled, published };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Turns one recipe's declared dependencies into the exact versions its index
|
|
654
|
-
* entry publishes.
|
|
655
|
-
*
|
|
656
|
-
* This is the whole point of recording dependencies in the index: a consumer
|
|
657
|
-
* installing version 1.4.0 of a recipe installs what 1.4.0 was released
|
|
658
|
-
* against, not whatever its ranges happen to reach months later. That is also
|
|
659
|
-
* why this runs only when a version is first recorded; an entry the index
|
|
660
|
-
* already publishes is carried forward and never resolved again.
|
|
661
|
-
*
|
|
662
|
-
* - A SIBLING with no range means "the version released alongside me", which
|
|
663
|
-
* is the version that sibling is settled on.
|
|
664
|
-
* - A SIBLING with a range resolves against the versions this repository
|
|
665
|
-
* publishes.
|
|
666
|
-
* - A CROSS-REPOSITORY dependency carries the identity of the repository it
|
|
667
|
-
* lives in, which is what a consumer needs in order to add that repository
|
|
668
|
-
* and find the recipe. Its exact version belongs to that repository's own
|
|
669
|
-
* index, so the range the manifest declared is recorded instead.
|
|
670
|
-
*
|
|
671
|
-
* @param declared - What the recipe's manifest declares.
|
|
672
|
-
* @param siblings - What each recipe here is published at, for this resolution.
|
|
673
|
-
*/
|
|
674
|
-
function resolveIndexDependencies(
|
|
675
|
-
declared: DeclaredDependencies,
|
|
676
|
-
siblings: SiblingState
|
|
677
|
-
): Record<string, IndexDependency> {
|
|
678
|
-
const resolved: Record<string, IndexDependency> = {};
|
|
679
|
-
|
|
680
|
-
for (const [key, dependency] of declared.byKey) {
|
|
681
|
-
const how = { declared: dependency.declared, kind: dependency.kind };
|
|
682
|
-
if (dependency.where === "remote") {
|
|
683
|
-
resolved[key] = { repo: dependency.repo, range: dependency.range, ...how };
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
if (dependency.range === undefined) {
|
|
687
|
-
const settled = siblings.settled.get(key);
|
|
688
|
-
resolved[key] = {
|
|
689
|
-
...(settled === undefined ? { range: "*" } : { version: settled }),
|
|
690
|
-
...how,
|
|
691
|
-
};
|
|
692
|
-
continue;
|
|
693
|
-
}
|
|
694
|
-
const best = semver.maxSatisfying(siblings.published.get(key) ?? [], dependency.range);
|
|
695
|
-
resolved[key] = {
|
|
696
|
-
...(best === null ? { range: dependency.range } : { version: best }),
|
|
697
|
-
...how,
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
return resolved;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Everything a version records about itself beyond its hash, tag and date: its
|
|
706
|
-
* dependencies, resolved and each with the entry that declared it, its
|
|
707
|
-
* manifest's `depends` and `subscribes` lists as written, and the variable
|
|
708
|
-
* definitions its manifest publishes. All are recorded even when empty, so a
|
|
709
|
-
* reader can tell "none" from "not recorded".
|
|
710
|
-
*
|
|
711
|
-
* @param recipe - The recipe, with the manifest of the version being recorded.
|
|
712
|
-
* @param declared - What that manifest declares.
|
|
713
|
-
* @param siblings - What each recipe here is published at, for this resolution.
|
|
714
|
-
*/
|
|
715
|
-
function describeVersion(
|
|
716
|
-
recipe: ValidatedRecipe,
|
|
717
|
-
declared: DeclaredDependencies,
|
|
718
|
-
siblings: SiblingState
|
|
719
|
-
): VersionDescription {
|
|
720
|
-
return {
|
|
721
|
-
dependencies: resolveIndexDependencies(declared, siblings),
|
|
722
|
-
depends: (recipe.manifest.depends ?? []).map((written) => written.trim()),
|
|
723
|
-
subscribes: (recipe.manifest.subscribes ?? []).map((written) => written.trim()),
|
|
724
|
-
variables: recipe.manifest.variables ?? [],
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/** What `describeVersion` records about a version. */
|
|
729
|
-
type VersionDescription = Pick<
|
|
730
|
-
IndexVersion,
|
|
731
|
-
"dependencies" | "depends" | "subscribes" | "variables"
|
|
732
|
-
>;
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Checks the manifest lists the index records for an already published
|
|
736
|
-
* version against the version's manifest. They are copied, not resolved, so
|
|
737
|
-
* any difference means the index was edited.
|
|
738
|
-
*
|
|
739
|
-
* @param key - The recipe's key, `namespace/recipe`.
|
|
740
|
-
* @param version - The published version.
|
|
741
|
-
* @param published - The version's index entry.
|
|
742
|
-
* @param recipe - The recipe, with the version's manifest.
|
|
743
|
-
*/
|
|
744
|
-
function checkRecordedLists(
|
|
745
|
-
key: string,
|
|
746
|
-
version: string,
|
|
747
|
-
published: IndexVersion,
|
|
748
|
-
recipe: ValidatedRecipe
|
|
749
|
-
): ValidationProblem[] {
|
|
750
|
-
const differences: string[] = [];
|
|
751
|
-
for (const list of ["depends", "subscribes"] as const) {
|
|
752
|
-
const recorded = published[list];
|
|
753
|
-
if (recorded === undefined) continue;
|
|
754
|
-
const declared = (recipe.manifest[list] ?? []).map((written) => written.trim());
|
|
755
|
-
if (JSON.stringify(recorded) === JSON.stringify(declared)) continue;
|
|
756
|
-
differences.push(
|
|
757
|
-
`'${list}': the index records [${recorded.join(", ")}], and the manifest declares ` +
|
|
758
|
-
`[${declared.join(", ")}]`
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
if (differences.length === 0) return [];
|
|
763
|
-
return [
|
|
764
|
-
{
|
|
765
|
-
level: "error",
|
|
766
|
-
where: `${INDEX_FILENAME} recipes['${key}'].versions['${version}']`,
|
|
767
|
-
message:
|
|
768
|
-
`version ${version} of '${key}' is already published, and the lists the index ` +
|
|
769
|
-
`records for it disagree with its manifest:\n` +
|
|
770
|
-
differences.map((line) => ` - ${line}\n`).join("") +
|
|
771
|
-
` A published version never changes. If the index was edited, restore the entry; ` +
|
|
772
|
-
`to publish different lists, bump the version in this manifest, or with ` +
|
|
773
|
-
`'sous repo release --bump patch'.`,
|
|
774
|
-
},
|
|
775
|
-
];
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Checks the dependencies the index records for an already published version
|
|
780
|
-
* against what that version's manifest declares.
|
|
781
|
-
*
|
|
782
|
-
* The recorded list is frozen, so it is never compared with a fresh
|
|
783
|
-
* resolution: a sibling released since, or a recipe a declared namespace has
|
|
784
|
-
* gained since, is exactly what freezing keeps out of it. What must still hold
|
|
785
|
-
* is that the list honours the manifest: every recipe the manifest names is
|
|
786
|
-
* there, nothing is there that the manifest does not declare, a sibling sits
|
|
787
|
-
* inside its declared range, and a dependency in another repository carries the
|
|
788
|
-
* repository and range the manifest wrote. A disagreement means the index was
|
|
789
|
-
* edited, or this sous reads the manifest differently from the one that
|
|
790
|
-
* published the version.
|
|
791
|
-
*
|
|
792
|
-
* @param key - The recipe's key, `namespace/recipe`.
|
|
793
|
-
* @param version - The published version.
|
|
794
|
-
* @param recorded - The dependencies the index records for it.
|
|
795
|
-
* @param declared - What the version's manifest declares.
|
|
796
|
-
*/
|
|
797
|
-
function checkRecordedDependencies(
|
|
798
|
-
key: string,
|
|
799
|
-
version: string,
|
|
800
|
-
recorded: Record<string, IndexDependency> | undefined,
|
|
801
|
-
declared: DeclaredDependencies
|
|
802
|
-
): ValidationProblem[] {
|
|
803
|
-
const differences: string[] = [];
|
|
804
|
-
const entries = recorded ?? {};
|
|
805
|
-
|
|
806
|
-
for (const [name, dependency] of declared.byKey) {
|
|
807
|
-
const entry = entries[name];
|
|
808
|
-
if (entry === undefined) {
|
|
809
|
-
if (dependency.where === "remote" || dependency.named) {
|
|
810
|
-
differences.push(
|
|
811
|
-
`'${name}': the manifest declares ${describeDeclared(dependency)}, and the index ` +
|
|
812
|
-
`records nothing for it`
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
continue;
|
|
816
|
-
}
|
|
817
|
-
if (!recordedSatisfies(entry, dependency)) {
|
|
818
|
-
differences.push(
|
|
819
|
-
`'${name}': the index records ${describeRecorded(entry)}, and the manifest declares ` +
|
|
820
|
-
`${describeDeclared(dependency)}`
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
for (const [name, entry] of Object.entries(entries)) {
|
|
826
|
-
if (declared.byKey.has(name)) continue;
|
|
827
|
-
// A recipe the declared namespace held when the version was published, and
|
|
828
|
-
// no longer does, is history rather than a disagreement.
|
|
829
|
-
const namespace = namespaceOfKey(name);
|
|
830
|
-
if (entry.repo === undefined && declared.namespaces.has(namespace)) continue;
|
|
831
|
-
differences.push(
|
|
832
|
-
`'${name}': the index records ${describeRecorded(entry)}, and the manifest does not ` +
|
|
833
|
-
`declare it`
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (differences.length === 0) return [];
|
|
838
|
-
return [
|
|
839
|
-
{
|
|
840
|
-
level: "error",
|
|
841
|
-
where: `${INDEX_FILENAME} recipes['${key}'].versions['${version}'].dependencies`,
|
|
842
|
-
message:
|
|
843
|
-
`version ${version} of '${key}' is already published, and the dependencies the index ` +
|
|
844
|
-
`records for it disagree with what its manifest declares:\n` +
|
|
845
|
-
differences.map((line) => ` - ${line}\n`).join("") +
|
|
846
|
-
` A published version never changes, so a release carries its dependencies forward ` +
|
|
847
|
-
`exactly as they were published. If the index was edited, restore the entry. To ` +
|
|
848
|
-
`publish different dependencies, bump the version in this manifest, or with ` +
|
|
849
|
-
`'sous repo release --bump patch'.`,
|
|
850
|
-
},
|
|
851
|
-
];
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/** True when a recorded dependency honours what the manifest declares for it. */
|
|
855
|
-
function recordedSatisfies(entry: IndexDependency, dependency: DeclaredDependency): boolean {
|
|
856
|
-
// How it was declared is recorded only since sous described recipes in the
|
|
857
|
-
// index; an entry that records it has to agree with the manifest too.
|
|
858
|
-
if (entry.declared !== undefined && entry.declared !== dependency.declared) return false;
|
|
859
|
-
if (entry.kind !== undefined && entry.kind !== dependency.kind) return false;
|
|
860
|
-
if (dependency.where === "remote") {
|
|
861
|
-
return (
|
|
862
|
-
entry.repo === dependency.repo &&
|
|
863
|
-
entry.range === dependency.range &&
|
|
864
|
-
entry.version === undefined
|
|
865
|
-
);
|
|
866
|
-
}
|
|
867
|
-
if (entry.repo !== undefined) return false;
|
|
868
|
-
if (dependency.range === undefined) return true;
|
|
869
|
-
if (entry.version !== undefined) return semver.satisfies(entry.version, dependency.range);
|
|
870
|
-
return entry.range === dependency.range;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
/** Describes a recorded dependency the way an author would say it. */
|
|
874
|
-
function describeRecorded(entry: IndexDependency): string {
|
|
875
|
-
const what =
|
|
876
|
-
entry.version !== undefined ? `version ${entry.version}` : `the range '${entry.range}'`;
|
|
877
|
-
const from = entry.repo === undefined ? what : `${what} from ${entry.repo}`;
|
|
878
|
-
return entry.declared === undefined || entry.kind === undefined
|
|
879
|
-
? from
|
|
880
|
-
: `${from}, through the entry '${entry.declared}' as a ${describeDependencyKind(entry.kind)}`;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
/** Describes a declared dependency the way an author would say it. */
|
|
884
|
-
function describeDeclared(dependency: DeclaredDependency): string {
|
|
885
|
-
const how =
|
|
886
|
-
`, through the entry '${dependency.declared}' as a ` +
|
|
887
|
-
describeDependencyKind(dependency.kind);
|
|
888
|
-
if (dependency.where === "remote") {
|
|
889
|
-
return `the range '${dependency.range}' from ${dependency.repo}${how}`;
|
|
890
|
-
}
|
|
891
|
-
return dependency.range === undefined
|
|
892
|
-
? `it with no range${how}`
|
|
893
|
-
: `the range '${dependency.range}'${how}`;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/** What rebuilding one tagged version from its tag produced. */
|
|
897
|
-
type RebuiltVersion = {
|
|
898
|
-
/** The content hash of the recipe folder at the tag. */
|
|
899
|
-
hash: string;
|
|
900
|
-
/**
|
|
901
|
-
* Its dependencies and variable definitions when the recipe could be read at
|
|
902
|
-
* the tag, and nothing when it could not; see `describeVersion`.
|
|
903
|
-
*/
|
|
904
|
-
description: VersionDescription;
|
|
905
|
-
/** A warning when they could not. */
|
|
906
|
-
problems: ValidationProblem[];
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
/**
|
|
910
|
-
* Rebuilds a tagged version the index does not record, from the tree its tag
|
|
911
|
-
* carries: the content hash of the recipe folder, and the dependencies its
|
|
912
|
-
* manifest declared there, resolved against the repository as it stood at the
|
|
913
|
-
* tag. That is as close as sous can get to what the release that cut the tag
|
|
914
|
-
* recorded; it is resolved once, then frozen like any other entry.
|
|
915
|
-
*
|
|
916
|
-
* @param rootDir - The repository's root directory.
|
|
917
|
-
* @param tag - The tag to rebuild from.
|
|
918
|
-
* @param recipe - The recipe as it stands now, for its folder and its key.
|
|
919
|
-
* @param tagsByKey - Every release tag, grouped by recipe.
|
|
920
|
-
* @param existing - The committed index, when there is one.
|
|
921
|
-
* @param run - The command runner git calls go through.
|
|
922
|
-
* @param settled - The dependencies that read more than one way, as the release settled them.
|
|
923
|
-
*/
|
|
924
|
-
async function rebuildTaggedVersion(
|
|
925
|
-
rootDir: string,
|
|
926
|
-
tag: RecipeTag,
|
|
927
|
-
recipe: ValidatedRecipe,
|
|
928
|
-
tagsByKey: Map<string, RecipeTag[]>,
|
|
929
|
-
existing: IndexFile | undefined,
|
|
930
|
-
run: RunOptions["run"],
|
|
931
|
-
settled?: Map<string, SettledDependency>
|
|
932
|
-
): Promise<RebuiltVersion> {
|
|
933
|
-
return withTaggedTree(
|
|
934
|
-
rootDir,
|
|
935
|
-
tag.tag,
|
|
936
|
-
".",
|
|
937
|
-
async (treeDir) => {
|
|
938
|
-
const hash = await hashDirectory(path.join(treeDir, ...recipe.path.split("/")));
|
|
939
|
-
|
|
940
|
-
let reason: string;
|
|
941
|
-
try {
|
|
942
|
-
const snapshot = validateRepo(treeDir);
|
|
943
|
-
const tagged = snapshot.recipes.find((entry) => entry.key === recipe.key);
|
|
944
|
-
if (tagged !== undefined) {
|
|
945
|
-
const description = describeVersion(
|
|
946
|
-
tagged,
|
|
947
|
-
declaredDependencies(tagged, snapshot, settled),
|
|
948
|
-
siblingState(snapshot, tagsByKey, existing)
|
|
949
|
-
);
|
|
950
|
-
return { hash, description, problems: [] };
|
|
951
|
-
}
|
|
952
|
-
reason = `The repository manifest at that tag does not list '${recipe.key}'.`;
|
|
953
|
-
} catch (error) {
|
|
954
|
-
reason = describeError(error);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
return {
|
|
958
|
-
hash,
|
|
959
|
-
description: {},
|
|
960
|
-
problems: [
|
|
961
|
-
{
|
|
962
|
-
level: "warning",
|
|
963
|
-
where: `${INDEX_FILENAME} recipes['${recipe.key}'].versions['${tag.version}']`,
|
|
964
|
-
message:
|
|
965
|
-
`version ${tag.version} was rebuilt from the tag '${tag.tag}' without its ` +
|
|
966
|
-
`dependencies, because sous could not read the recipe at that tag.\n ${reason}`,
|
|
967
|
-
},
|
|
968
|
-
],
|
|
969
|
-
};
|
|
970
|
-
},
|
|
971
|
-
{ run }
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
/** Groups release tags by the recipe key they belong to. */
|
|
976
|
-
function groupTagsByKey(tags: ReadonlyArray<RecipeTag>): Map<string, RecipeTag[]> {
|
|
977
|
-
const grouped = new Map<string, RecipeTag[]>();
|
|
978
|
-
for (const tag of tags) {
|
|
979
|
-
const key = recipeTagKey(tag);
|
|
980
|
-
const list = grouped.get(key);
|
|
981
|
-
if (list === undefined) grouped.set(key, [tag]);
|
|
982
|
-
else list.push(tag);
|
|
983
|
-
}
|
|
984
|
-
return grouped;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
/** The content hash of a recipe folder as it stood at a tag. */
|
|
988
|
-
async function hashTaggedRecipe(
|
|
989
|
-
rootDir: string,
|
|
990
|
-
tag: string,
|
|
991
|
-
recipePath: string,
|
|
992
|
-
run: RunOptions["run"]
|
|
993
|
-
): Promise<string> {
|
|
994
|
-
return withTaggedTree(rootDir, tag, recipePath, (dir) => hashDirectory(dir), { run });
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
/** The `releasedAt` field for a tag, or nothing when git cannot date it. */
|
|
998
|
-
async function releasedAtOf(
|
|
999
|
-
rootDir: string,
|
|
1000
|
-
tag: string,
|
|
1001
|
-
run: RunOptions["run"]
|
|
1002
|
-
): Promise<{ releasedAt?: string }> {
|
|
1003
|
-
const date = await tagCommitDate(rootDir, tag, { run });
|
|
1004
|
-
return date === undefined ? {} : { releasedAt: date };
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* Confirms the recipe manifest carried by a tag declares the version the tag
|
|
1009
|
-
* names. A tag whose metadata says something else is the exact failure the
|
|
1010
|
-
* design forbids: a version hidden behind a tag that disagrees with it.
|
|
1011
|
-
*/
|
|
1012
|
-
async function checkTaggedMetadata(
|
|
1013
|
-
rootDir: string,
|
|
1014
|
-
tag: string,
|
|
1015
|
-
recipePath: string,
|
|
1016
|
-
manifestName: string,
|
|
1017
|
-
version: string,
|
|
1018
|
-
where: string,
|
|
1019
|
-
run: RunOptions["run"]
|
|
1020
|
-
): Promise<ValidationProblem[]> {
|
|
1021
|
-
const text = await readFileAtTag(rootDir, tag, `${recipePath}/${manifestName}`, { run });
|
|
1022
|
-
if (text === undefined) {
|
|
1023
|
-
return [
|
|
1024
|
-
{
|
|
1025
|
-
level: "error",
|
|
1026
|
-
where,
|
|
1027
|
-
message:
|
|
1028
|
-
`the tag '${tag}' does not carry a '${manifestName}' at '${recipePath}'. The tag ` +
|
|
1029
|
-
`and the recipe metadata must agree about what was published.`,
|
|
1030
|
-
},
|
|
1031
|
-
];
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
let taggedVersion: string | undefined;
|
|
1035
|
-
try {
|
|
1036
|
-
const raw = manifestName.endsWith(".json")
|
|
1037
|
-
? parseJsoncText(text, `${tag}:${recipePath}/${manifestName}`)
|
|
1038
|
-
: parseYamlText(text, `${tag}:${recipePath}/${manifestName}`);
|
|
1039
|
-
taggedVersion = parseRecipeManifest(raw, `${tag}:${recipePath}/${manifestName}`).version;
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
return [
|
|
1042
|
-
{
|
|
1043
|
-
level: "warning",
|
|
1044
|
-
where,
|
|
1045
|
-
message:
|
|
1046
|
-
`the recipe manifest at the tag '${tag}' could not be read, so sous cannot confirm ` +
|
|
1047
|
-
`the tag and the metadata agree.\n ${describeError(error)}`,
|
|
1048
|
-
},
|
|
1049
|
-
];
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
if (taggedVersion !== version) {
|
|
1053
|
-
return [
|
|
1054
|
-
{
|
|
1055
|
-
level: "error",
|
|
1056
|
-
where,
|
|
1057
|
-
message:
|
|
1058
|
-
`the tag '${tag}' carries a manifest declaring version ${taggedVersion}, not ` +
|
|
1059
|
-
`${version}. Recipe metadata is the source of truth for versions, so the tag is ` +
|
|
1060
|
-
`wrong; delete it and let 'sous repo release' cut it again.`,
|
|
1061
|
-
},
|
|
1062
|
-
];
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
return [];
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/**
|
|
1069
|
-
* Reports release tags that belong to no recipe this repository publishes any
|
|
1070
|
-
* more. That is ordinary history after a recipe is renamed or retired, so it is
|
|
1071
|
-
* a warning: it is worth saying, and it is not worth stopping for.
|
|
1072
|
-
*/
|
|
1073
|
-
function checkOrphanTags(
|
|
1074
|
-
validation: RepoValidation,
|
|
1075
|
-
tags: ReadonlyArray<RecipeTag>
|
|
1076
|
-
): ValidationProblem[] {
|
|
1077
|
-
const known = new Set(validation.recipes.map((recipe) => recipe.key));
|
|
1078
|
-
const orphaned = new Map<string, string[]>();
|
|
1079
|
-
|
|
1080
|
-
for (const tag of tags) {
|
|
1081
|
-
const key = recipeTagKey(tag);
|
|
1082
|
-
if (known.has(key)) continue;
|
|
1083
|
-
const list = orphaned.get(key);
|
|
1084
|
-
if (list === undefined) orphaned.set(key, [tag.tag]);
|
|
1085
|
-
else list.push(tag.tag);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
return [...orphaned.entries()].map(([key, tagNames]) => ({
|
|
1089
|
-
level: "warning" as const,
|
|
1090
|
-
where: `tags for '${key}'`,
|
|
1091
|
-
message:
|
|
1092
|
-
`this repository carries ${tagNames.length} release ${
|
|
1093
|
-
tagNames.length === 1 ? "tag" : "tags"
|
|
1094
|
-
} for '${key}' (${tagNames.join(", ")}), which it no longer publishes. That is normal ` +
|
|
1095
|
-
`after a recipe is renamed or retired; the tags are left alone.`,
|
|
1096
|
-
}));
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/** True when two indexes differ only in when they were generated. */
|
|
1100
|
-
function sameExceptStamp(left: IndexFile, right: IndexFile): boolean {
|
|
1101
|
-
const strip = (index: IndexFile) => stringifyIndexFile({ ...index, generatedAt: "" });
|
|
1102
|
-
return strip(left) === strip(right);
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
/** Renders an absolute path as a repository-relative one, with forward slashes. */
|
|
1106
|
-
function relativeTo(rootDir: string, target: string): string {
|
|
1107
|
-
return path.relative(rootDir, target).split(path.sep).join("/");
|
|
1108
|
-
}
|