Skip to content

Commit 88cbc17

Browse files
authored
fix version sorting and report major version in outdated (#670)
* fix version sorting and report major version in outdated * convert wantedMajor and latestMajor to string * update test to expect just the full semver version in the publishedVersions object * break into a new function * update test and some names for clarity * more consistent naming
1 parent 5a5a9b2 commit 88cbc17

File tree

10 files changed

+218
-76
lines changed

10 files changed

+218
-76
lines changed

src/spec-configuration/containerCollectionsOCI.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -433,9 +433,28 @@ async function getJsonWithMimeType<T>(params: CommonParams, url: string, ref: OC
433433
}
434434
}
435435

436-
// Lists published versions/tags of a feature/template
436+
// Gets published tags and sorts them by ascending semantic version.
437+
// Omits any tags (eg: 'latest', or major/minor tags '1','1.0') that are not semantic versions.
438+
export async function getVersionsStrictSorted(params: CommonParams, ref: OCIRef): Promise<string[] | undefined> {
439+
const { output } = params;
440+
441+
const publishedTags = await getPublishedTags(params, ref);
442+
if (!publishedTags) {
443+
return;
444+
}
445+
446+
const sortedVersions = publishedTags
447+
.filter(f => semver.valid(f)) // Remove all major,minor,latest tags
448+
.sort((a, b) => semver.compare(a, b));
449+
450+
output.write(`Published versions (sorted) for '${ref.id}': ${JSON.stringify(sortedVersions, undefined, 2)}`, LogLevel.Trace);
451+
452+
return sortedVersions;
453+
}
454+
455+
// Lists published tags of a Feature/Template
437456
// Specification: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#content-discovery
438-
export async function getPublishedVersions(params: CommonParams, ref: OCIRef, sorted: boolean = false): Promise<string[] | undefined> {
457+
export async function getPublishedTags(params: CommonParams, ref: OCIRef): Promise<string[] | undefined> {
439458
const { output } = params;
440459
try {
441460
const url = `https://${ref.registry}/v2/${ref.namespace}/${ref.id}/tags/list`;
@@ -470,18 +489,10 @@ export async function getPublishedVersions(params: CommonParams, ref: OCIRef, so
470489

471490
const publishedVersionsResponse: OCITagList = JSON.parse(body);
472491

473-
if (!sorted) {
474-
return publishedVersionsResponse.tags;
475-
}
476-
477-
// Sort tags in descending order, removing latest.
478-
const hasLatest = publishedVersionsResponse.tags.includes('latest');
479-
const sortedVersions = publishedVersionsResponse.tags
480-
.filter(f => f !== 'latest')
481-
.sort((a, b) => semver.compareIdentifiers(a, b));
482-
483-
484-
return hasLatest ? ['latest', ...sortedVersions] : sortedVersions;
492+
// Return published tags from the registry as-is, meaning:
493+
// - Not necessarily sorted
494+
// - *Including* major/minor/latest tags
495+
return publishedVersionsResponse.tags;
485496
} catch (e) {
486497
output.write(`Failed to parse published versions: ${e}`, LogLevel.Error);
487498
return;

src/spec-configuration/containerFeaturesConfiguration.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Log, LogLevel } from '../spec-utils/log';
1717
import { request } from '../spec-utils/httpRequest';
1818
import { fetchOCIFeature, tryGetOCIFeatureSet, fetchOCIFeatureManifestIfExistsFromUserIdentifier } from './containerFeaturesOCI';
1919
import { uriToFsPath } from './configurationCommonUtils';
20-
import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getPublishedVersions, getRef } from './containerCollectionsOCI';
20+
import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getRef, getVersionsStrictSorted } from './containerCollectionsOCI';
2121
import { Lockfile, readLockfile, writeLockfile } from './lockfile';
2222
import { computeDependsOnInstallationOrder } from './containerFeaturesOrder';
2323
import { logFeatureAdvisories } from './featureAdvisories';
@@ -591,7 +591,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co
591591
const updatedFeatureId = getBackwardCompatibleFeatureId(output, userFeatureId);
592592
const featureRef = getRef(output, updatedFeatureId);
593593
if (featureRef) {
594-
const versions = (await getPublishedVersions(params, featureRef, true))
594+
const versions = (await getVersionsStrictSorted(params, featureRef))
595595
?.reverse();
596596
if (versions) {
597597
const lockfileVersion = lockfile?.features[userFeatureId]?.version;
@@ -613,7 +613,9 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co
613613
features[userFeatureId] = {
614614
current: lockfileVersion || wanted,
615615
wanted,
616+
wantedMajor: wanted && semver.major(wanted)?.toString(),
616617
latest: versions[0],
618+
latestMajor: semver.major(versions[0])?.toString(),
617619
};
618620
}
619621
}

src/spec-node/collectionCommonUtils/publishCommandImpl.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import path from 'path';
22
import * as semver from 'semver';
33
import { Log, LogLevel } from '../../spec-utils/log';
4-
import { CommonParams, getPublishedVersions, OCICollectionRef, OCIRef } from '../../spec-configuration/containerCollectionsOCI';
4+
import { CommonParams, getPublishedTags, OCICollectionRef, OCIRef } from '../../spec-configuration/containerCollectionsOCI';
55
import { OCICollectionFileName } from './packageCommandImpl';
66
import { pushCollectionMetadata, pushOCIFeatureOrTemplate } from '../../spec-configuration/containerCollectionsOCIPush';
77

88
let semanticVersions: string[] = [];
9-
function updateSemanticVersionsList(publishedVersions: string[], version: string, range: string, publishVersion: string) {
9+
function updateSemanticTagsList(publishedTags: string[], version: string, range: string, publishVersion: string) {
1010
// Reference: https://github.com/npm/node-semver#ranges-1
11-
const publishedMaxVersion = semver.maxSatisfying(publishedVersions, range);
11+
const publishedMaxVersion = semver.maxSatisfying(publishedTags, range);
1212
if (publishedMaxVersion === null || semver.compare(version, publishedMaxVersion) === 1) {
1313
semanticVersions.push(publishVersion);
1414
}
1515
return;
1616
}
1717

18-
export function getSemanticVersions(version: string, publishedVersions: string[], output: Log) {
19-
if (publishedVersions.includes(version)) {
18+
export function getSemanticTags(version: string, tags: string[], output: Log) {
19+
if (tags.includes(version)) {
2020
output.write(`(!) WARNING: Version ${version} already exists, skipping ${version}...`, LogLevel.Warning);
2121
return undefined;
2222
}
@@ -31,10 +31,10 @@ export function getSemanticVersions(version: string, publishedVersions: string[]
3131

3232
// Adds semantic versions depending upon the existings (published) versions
3333
// eg. 1.2.3 --> [1, 1.2, 1.2.3, latest]
34-
updateSemanticVersionsList(publishedVersions, version, `${parsedVersion.major}.x.x`, `${parsedVersion.major}`);
35-
updateSemanticVersionsList(publishedVersions, version, `${parsedVersion.major}.${parsedVersion.minor}.x`, `${parsedVersion.major}.${parsedVersion.minor}`);
34+
updateSemanticTagsList(tags, version, `${parsedVersion.major}.x.x`, `${parsedVersion.major}`);
35+
updateSemanticTagsList(tags, version, `${parsedVersion.major}.${parsedVersion.minor}.x`, `${parsedVersion.major}.${parsedVersion.minor}`);
3636
semanticVersions.push(version);
37-
updateSemanticVersionsList(publishedVersions, version, `x.x.x`, 'latest');
37+
updateSemanticTagsList(tags, version, `x.x.x`, 'latest');
3838

3939
return semanticVersions;
4040
}
@@ -43,24 +43,24 @@ export async function doPublishCommand(params: CommonParams, version: string, oc
4343
const { output } = params;
4444

4545
output.write(`Fetching published versions...`, LogLevel.Info);
46-
const publishedVersions = await getPublishedVersions(params, ociRef);
46+
const publishedTags = await getPublishedTags(params, ociRef);
4747

48-
if (!publishedVersions) {
48+
if (!publishedTags) {
4949
return;
5050
}
5151

52-
const semanticVersions: string[] | undefined = getSemanticVersions(version, publishedVersions, output);
52+
const semanticTags: string[] | undefined = getSemanticTags(version, publishedTags, output);
5353

54-
if (!!semanticVersions) {
55-
output.write(`Publishing versions: ${semanticVersions.toString()}...`, LogLevel.Info);
54+
if (!!semanticTags) {
55+
output.write(`Publishing tags: ${semanticTags.toString()}...`, LogLevel.Info);
5656
const pathToTgz = path.join(outputDir, archiveName);
57-
const digest = await pushOCIFeatureOrTemplate(params, ociRef, pathToTgz, semanticVersions, collectionType, featureAnnotations);
57+
const digest = await pushOCIFeatureOrTemplate(params, ociRef, pathToTgz, semanticTags, collectionType, featureAnnotations);
5858
if (!digest) {
5959
output.write(`(!) ERR: Failed to publish ${collectionType}: '${ociRef.resource}'`, LogLevel.Error);
6060
return;
6161
}
6262
output.write(`Published ${collectionType}: '${ociRef.id}'`, LogLevel.Info);
63-
return { publishedVersions: semanticVersions, digest };
63+
return { publishedTags: semanticTags, digest };
6464
}
6565

6666
return {}; // Not an error if no versions were published, likely they just already existed and were skipped.

src/spec-node/featuresCLI/info.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Argv } from 'yargs';
2-
import { OCIManifest, OCIRef, fetchOCIManifestIfExists, getPublishedVersions, getRef } from '../../spec-configuration/containerCollectionsOCI';
2+
import { OCIManifest, OCIRef, fetchOCIManifestIfExists, getPublishedTags, getRef } from '../../spec-configuration/containerCollectionsOCI';
33
import { Log, LogLevel, mapLogLevel } from '../../spec-utils/log';
44
import { getPackageConfig } from '../../spec-utils/product';
55
import { createLog } from '../devContainers';
@@ -27,7 +27,7 @@ export function featuresInfoHandler(args: FeaturesInfoArgs) {
2727
interface InfoJsonOutput {
2828
manifest?: OCIManifest;
2929
canonicalId?: string;
30-
publishedVersions?: string[];
30+
publishedTags?: string[];
3131
}
3232

3333
async function featuresInfo({
@@ -86,12 +86,12 @@ async function featuresInfo({
8686

8787
// --- Get all published tags for resource
8888
if (mode === 'tags' || mode === 'verbose') {
89-
const publishedVersions = await getTags(params, featureRef);
89+
const publishedTags = await getTags(params, featureRef);
9090
if (outputFormat === 'text') {
91-
console.log(encloseStringInBox('Published Version'));
92-
console.log(`${publishedVersions.join('\n ')}`);
91+
console.log(encloseStringInBox('Published Tags'));
92+
console.log(`${publishedTags.join('\n ')}`);
9393
} else {
94-
jsonOutput.publishedVersions = publishedVersions;
94+
jsonOutput.publishedTags = publishedTags;
9595
}
9696
}
9797

@@ -145,16 +145,16 @@ async function getManifest(params: { output: Log; env: NodeJS.ProcessEnv; output
145145

146146
async function getTags(params: { output: Log; env: NodeJS.ProcessEnv; outputFormat: string }, featureRef: OCIRef) {
147147
const { outputFormat } = params;
148-
const publishedVersions = await getPublishedVersions(params, featureRef, true);
149-
if (!publishedVersions || publishedVersions.length === 0) {
148+
const publishedTags = await getPublishedTags(params, featureRef);
149+
if (!publishedTags || publishedTags.length === 0) {
150150
if (outputFormat === 'json') {
151151
console.log(JSON.stringify({}));
152152
} else {
153153
console.log(`No published versions found for feature '${featureRef.resource}'\n`);
154154
}
155155
process.exit(1);
156156
}
157-
return publishedVersions;
157+
return publishedTags;
158158
}
159159

160160
function encloseStringInBox(str: string, indent: number = 0) {

src/spec-node/featuresCLI/publish.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async function featuresPublish({
100100
process.exit(1);
101101
}
102102

103-
const isPublished = (publishResult?.digest && publishResult?.publishedVersions.length > 0);
103+
const isPublished = (publishResult?.digest && publishResult?.publishedTags.length > 0);
104104
let thisResult = isPublished ? {
105105
...publishResult,
106106
version: f.version,
@@ -126,7 +126,7 @@ async function featuresPublish({
126126
process.exit(1);
127127
}
128128

129-
if (publishResult?.digest && publishResult?.publishedVersions.length > 0) {
129+
if (publishResult?.digest && publishResult?.publishedTags.length > 0) {
130130
publishedLegacyIds.push(legacyId);
131131
}
132132
}

src/spec-node/templatesCLI/publish.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ async function templatesPublish({
9494
process.exit(1);
9595
}
9696

97-
const thisResult = (publishResult?.digest && publishResult?.publishedVersions?.length > 0) ? {
97+
const thisResult = (publishResult?.digest && publishResult?.publishedTags?.length > 0) ? {
9898
...publishResult,
9999
version: t.version,
100100
} : {};

src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"ghcr.io/devcontainers/features/git:1.0": "latest",
55
"ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": "latest",
66
"ghcr.io/devcontainers/features/github-cli": "latest",
7-
"ghcr.io/devcontainers/features/azure-cli:0": "latest"
7+
"ghcr.io/devcontainers/features/azure-cli:0": "latest",
8+
"ghcr.io/codspace/versioning/foo:0.3.1": "latest"
89
}
910
}

src/test/container-features/containerFeaturesOCIPush.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const output = makeLog(createPlainLog(text => process.stdout.write(text),
1515
const testAssetsDir = `${__dirname}/assets`;
1616

1717
interface PublishResult {
18-
publishedVersions: string[];
18+
publishedTags: string[];
1919
digest: string;
2020
version: string;
2121
publishedLegacyIds?: string[];
@@ -87,7 +87,7 @@ registry`;
8787
const color = result['color'];
8888
assert.isDefined(color);
8989
assert.isDefined(color.digest);
90-
assert.deepEqual(color.publishedVersions, [
90+
assert.deepEqual(color.publishedTags, [
9191
'1',
9292
'1.0',
9393
'1.0.0',
@@ -99,7 +99,7 @@ registry`;
9999
const hello = result['hello'];
100100
assert.isDefined(hello);
101101
assert.isDefined(hello.digest);
102-
assert.deepEqual(hello.publishedVersions, [
102+
assert.deepEqual(hello.publishedTags, [
103103
'1',
104104
'1.0',
105105
'1.0.0',
@@ -123,8 +123,8 @@ registry`;
123123
assert.isTrue(success);
124124
assert.isDefined(infoTagsResult);
125125
const tags = JSON.parse(infoTagsResult.stdout);
126-
const publishedVersions: string[] = tags['publishedVersions'];
127-
assert.equal(publishedVersions.length, 4);
126+
const publishedTags: string[] = tags['publishedTags'];
127+
assert.equal(publishedTags.length, 4);
128128

129129
success = false; // Reset success flag.
130130
try {
@@ -172,15 +172,15 @@ registry`;
172172
assert.isObject(color);
173173
// Check that the color object has no properties
174174
assert.isUndefined(color.digest);
175-
assert.isUndefined(color.publishedVersions);
175+
assert.isUndefined(color.publishedTags);
176176
assert.isUndefined(color.version);
177177

178178
// -- The breakfix version of hello was updated, so major and minor should be published again, too.
179179
const hello = result['hello'];
180180
assert.isDefined(hello);
181181
assert.isDefined(hello.digest);
182-
assert.isArray(hello.publishedVersions);
183-
assert.deepEqual(hello.publishedVersions, [
182+
assert.isArray(hello.publishedTags);
183+
assert.deepEqual(hello.publishedTags, [
184184
'1',
185185
'1.0',
186186
'1.0.1',
@@ -219,7 +219,7 @@ registry`;
219219
const newColor = result['new-color'];
220220
assert.isDefined(newColor);
221221
assert.isDefined(newColor.digest);
222-
assert.deepEqual(newColor.publishedVersions, [
222+
assert.deepEqual(newColor.publishedTags, [
223223
'1',
224224
'1.0',
225225
'1.0.1',
@@ -234,7 +234,7 @@ registry`;
234234
const hello = result['hello'];
235235
assert.isDefined(hello);
236236
assert.isDefined(hello.digest);
237-
assert.deepEqual(hello.publishedVersions, [
237+
assert.deepEqual(hello.publishedTags, [
238238
'1',
239239
'1.0',
240240
'1.0.0',
@@ -299,8 +299,8 @@ registry`;
299299
assert.isTrue(success);
300300
assert.isDefined(infoTagsResult);
301301
const tags = JSON.parse(infoTagsResult.stdout);
302-
const publishedVersions: string[] = tags['publishedVersions'];
303-
assert.equal(publishedVersions.length, 4);
302+
const publishedTags: string[] = tags['publishedTags'];
303+
assert.equal(publishedTags.length, 4);
304304
});
305305
});
306306

0 commit comments

Comments
 (0)