mirror of
https://github.com/badges/shields.git
synced 2025-04-18 19:44:04 +03:00
add [WingetVersion] Badge (#10245)
* feat: add winget version badge * chore: accept dotted path instead of slashed * test: add test for winget-version * fix: remove debug code * chore: use winget-specific version compare algorithm * fix: support latest and unknown * fix(winget/version): trailing '.0' handling is incorrect * fix(winget/version): latest returns last newest version instead of the first newest version * fix(winget/version): confusing subpackage and version name * fix(winget/version): example for latest is incorrect * add a couple of extra test cases for latest() --------- Co-authored-by: chris48s <git@chris-shaw.dev>
This commit is contained in:
parent
4ec62fa445
commit
00d72da97e
172
services/winget/version.js
Normal file
172
services/winget/version.js
Normal file
@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Comparing versions with winget's version comparator.
|
||||
*
|
||||
* See https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp for original implementation.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* Compares two strings representing version numbers lexicographically and returns an integer value.
|
||||
*
|
||||
* @param {string} v1 - The first version to compare
|
||||
* @param {string} v2 - The second version to compare
|
||||
* @returns {number} -1 if v1 is smaller than v2, 1 if v1 is larger than v2, 0 if v1 and v2 are equal
|
||||
* @example
|
||||
* compareVersion('1.2.3', '1.2.4') // returns -1 because numeric part of first version is smaller than the numeric part of second version.
|
||||
*/
|
||||
function compareVersion(v1, v2) {
|
||||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L109-L173
|
||||
// This implementation does not parse s_Approximate_Greater_Than
|
||||
// and s_Approximate_Less_Than since they won't appear in directory name (package version parsed by shields.io)
|
||||
const v1Trimmed = trimPrefix(v1)
|
||||
const v2Trimmed = trimPrefix(v2)
|
||||
|
||||
const v1Latest = v1Trimmed.trim().toLowerCase() === 'latest'
|
||||
const v2Latest = v2Trimmed.trim().toLowerCase() === 'latest'
|
||||
|
||||
if (v1Latest && v2Latest) {
|
||||
return 0
|
||||
} else if (v1Latest) {
|
||||
return 1
|
||||
} else if (v2Latest) {
|
||||
return -1
|
||||
}
|
||||
|
||||
const v1Unknown = v1Trimmed.trim().toLowerCase() === 'unknown'
|
||||
const v2Unknown = v2Trimmed.trim().toLowerCase() === 'unknown'
|
||||
|
||||
if (v1Unknown && v2Unknown) {
|
||||
return 0
|
||||
} else if (v1Unknown) {
|
||||
return -1
|
||||
} else if (v2Unknown) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const parts1 = v1Trimmed.split('.')
|
||||
const parts2 = v2Trimmed.split('.')
|
||||
|
||||
trimLastZeros(parts1)
|
||||
trimLastZeros(parts2)
|
||||
|
||||
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) {
|
||||
const part1 = parts1[i]
|
||||
const part2 = parts2[i]
|
||||
|
||||
const compare = compareVersionPart(part1, part2)
|
||||
if (compare !== 0) {
|
||||
return compare
|
||||
}
|
||||
}
|
||||
|
||||
if (parts1.length === parts2.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (parts1.length > parts2.length) {
|
||||
return 1
|
||||
} else if (parts1.length < parts2.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all leading non-digit characters from a version number string
|
||||
* if there is a digit before the split character, or no split characters exist.
|
||||
*
|
||||
* @param {string} version The version number string to trim
|
||||
* @returns {string} The version number string with all leading non-digit characters removed
|
||||
*/
|
||||
function trimPrefix(version) {
|
||||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L66
|
||||
// If there is a digit before the split character, or no split characters exist, trim off all leading non-digit characters
|
||||
|
||||
const digitPos = version.match(/(\d.*)/)
|
||||
const splitPos = version.match(/\./)
|
||||
if (digitPos && (splitPos == null || digitPos.index < splitPos.index)) {
|
||||
// there is digit before the split character so strip off all leading non-digit characters
|
||||
return version.slice(digitPos.index)
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all trailing zeros from a version number part array.
|
||||
*
|
||||
* @param {string[]} parts - parts
|
||||
*/
|
||||
function trimLastZeros(parts) {
|
||||
while (parts.length > 1 && parts[parts.length - 1].trim() === '0') {
|
||||
parts.pop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two strings representing version number parts lexicographically and returns an integer value.
|
||||
*
|
||||
* @param {string} part1 - The first version part to compare
|
||||
* @param {string} part2 - The second version part to compare
|
||||
* @returns {number} -1 if part1 is smaller than part2, 1 if part1 is larger than part2, 0 if part1 and part2 are equal
|
||||
* @example
|
||||
* compareVersionPart('3', '4') // returns -1 because numeric part of first part is smaller than the numeric part of second part.
|
||||
*/
|
||||
function compareVersionPart(part1, part2) {
|
||||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L324-L352
|
||||
const [, numericString1, other1] = part1.trim().match(/^(\d*)(.*)$/)
|
||||
const [, numericString2, other2] = part2.trim().match(/^(\d*)(.*)$/)
|
||||
const numeric1 = parseInt(numericString1 || '0', 10)
|
||||
const numeric2 = parseInt(numericString2 || '0', 10)
|
||||
|
||||
if (numeric1 < numeric2) {
|
||||
return -1
|
||||
} else if (numeric1 > numeric2) {
|
||||
return 1
|
||||
}
|
||||
// numeric1 === numeric2
|
||||
|
||||
const otherFolded1 = (other1 ?? '').toLowerCase()
|
||||
const otherFolded2 = (other2 ?? '').toLowerCase()
|
||||
|
||||
if (otherFolded1.length !== 0 && otherFolded2.length === 0) {
|
||||
return -1
|
||||
} else if (otherFolded1.length === 0 && otherFolded2.length !== 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (otherFolded1 < otherFolded2) {
|
||||
return -1
|
||||
} else if (otherFolded1 > otherFolded2) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the largest version number lexicographically from an array of strings representing version numbers and returns it as a string.
|
||||
*
|
||||
* @param {string[]} versions - The array of version numbers to compare
|
||||
* @returns {string|undefined} The largest version number as a string, or undefined if the array is empty
|
||||
* @example
|
||||
* latest(['1.2.3', '1.2.4', '1.3', '2.0']) // returns '2.0' because it is the largest version number.
|
||||
* latest(['1.2.3', '1.2.4', '1.3-alpha', '2.0-beta']) // returns '2.0-beta'. there is no special handling for pre-release versions.
|
||||
*/
|
||||
function latest(versions) {
|
||||
const len = versions.length
|
||||
if (len === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let version = versions[0]
|
||||
for (let i = 1; i < len; i++) {
|
||||
if (compareVersion(version, versions[i]) <= 0) {
|
||||
version = versions[i]
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
export { latest, compareVersion }
|
57
services/winget/version.spec.js
Normal file
57
services/winget/version.spec.js
Normal file
@ -0,0 +1,57 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import { compareVersion, latest } from './version.js'
|
||||
|
||||
describe('Winget Version helpers', function () {
|
||||
test(compareVersion, () => {
|
||||
// basic compare
|
||||
// https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerCLITests/Versions.cpp#L147
|
||||
given('1', '2').expect(-1)
|
||||
given('1.0.0', '2.0.0').expect(-1)
|
||||
given('0.0.1', '0.0.2').expect(-1)
|
||||
given('0.0.1-alpha', '0.0.2-alpha').expect(-1)
|
||||
given('0.0.1-beta', '0.0.2-alpha').expect(-1)
|
||||
given('0.0.1-beta', '0.0.2-alpha').expect(-1)
|
||||
given('13.9.8', '14.1').expect(-1)
|
||||
|
||||
given('1.0', '1.0.0').expect(0)
|
||||
|
||||
// Ensure whitespace doesn't affect equality
|
||||
given('1.0', '1.0 ').expect(0)
|
||||
given('1.0', '1. 0').expect(0)
|
||||
|
||||
// Ensure versions with preambles are sorted correctly
|
||||
given('1.0', 'Version 1.0').expect(0)
|
||||
given('foo1', 'bar1').expect(0)
|
||||
given('v0.0.1', '0.0.2').expect(-1)
|
||||
given('v0.0.1', 'v0.0.2').expect(-1)
|
||||
given('1.a2', '1.b1').expect(-1)
|
||||
given('alpha', 'beta').expect(-1)
|
||||
|
||||
// latest
|
||||
// https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerCLITests/Versions.cpp#L217
|
||||
given('1.0', 'latest').expect(-1)
|
||||
given('100', 'latest').expect(-1)
|
||||
given('943849587389754876.1', 'latest').expect(-1)
|
||||
given('latest', 'LATEST').expect(0)
|
||||
|
||||
// unknown
|
||||
// https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerCLITests/Versions.cpp#L231
|
||||
given('unknown', '1.0').expect(-1)
|
||||
given('unknown', '1.fork').expect(-1)
|
||||
given('unknown', 'UNKNOWN').expect(0)
|
||||
|
||||
// porting failure tests
|
||||
// https://github.com/badges/shields/pull/10245#discussion_r1817931237
|
||||
// trailing .0 and .0-beta
|
||||
given('1.6.0', '1.6.0-beta.98').expect(-1)
|
||||
})
|
||||
|
||||
test(latest, () => {
|
||||
given(['1.2.3', '1.2.4', '2.0', '1.3.9.1']).expect('2.0')
|
||||
given(['1.2.3', '1.2.4', '2.0-beta', '1.3-alpha']).expect('2.0-beta')
|
||||
|
||||
// compareVersion('3.1.1.0', '3.1.1') == 0, so It's free to choose any of them.
|
||||
// I don't know why but it looks winget registry uses last newest version.
|
||||
given(['3.1.1.0', '3.1.1']).expect('3.1.1')
|
||||
})
|
||||
})
|
120
services/winget/winget-version.service.js
Normal file
120
services/winget/winget-version.service.js
Normal file
@ -0,0 +1,120 @@
|
||||
import Joi from 'joi'
|
||||
import gql from 'graphql-tag'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { InvalidParameter, pathParam } from '../index.js'
|
||||
import { GithubAuthV4Service } from '../github/github-auth-service.js'
|
||||
import { transformErrors } from '../github/github-helpers.js'
|
||||
import { latest } from './version.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
repository: Joi.object({
|
||||
object: Joi.object({
|
||||
entries: Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
object: Joi.object({
|
||||
entries: Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
}),
|
||||
),
|
||||
}).required(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.allow(null)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class WingetVersion extends GithubAuthV4Service {
|
||||
static category = 'version'
|
||||
|
||||
static route = {
|
||||
base: 'winget/v',
|
||||
pattern: ':name',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/winget/v/{name}': {
|
||||
get: {
|
||||
summary: 'WinGet Package Version',
|
||||
description: 'WinGet Community Repository',
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'name',
|
||||
example: 'Microsoft.WSL',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'winget',
|
||||
}
|
||||
|
||||
async fetch({ name }) {
|
||||
const nameFirstLower = name[0].toLowerCase()
|
||||
const nameSlashed = name.replaceAll('.', '/')
|
||||
const path = `manifests/${nameFirstLower}/${nameSlashed}`
|
||||
const expression = `HEAD:${path}`
|
||||
return this._requestGraphql({
|
||||
query: gql`
|
||||
query RepoFiles($expression: String!) {
|
||||
repository(owner: "microsoft", name: "winget-pkgs") {
|
||||
object(expression: $expression) {
|
||||
... on Tree {
|
||||
entries {
|
||||
type
|
||||
name
|
||||
object {
|
||||
... on Tree {
|
||||
entries {
|
||||
type
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { expression },
|
||||
schema,
|
||||
transformErrors,
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ name }) {
|
||||
const json = await this.fetch({ name })
|
||||
if (json.data.repository.object?.entries == null) {
|
||||
throw new InvalidParameter({
|
||||
prettyMessage: 'package not found',
|
||||
})
|
||||
}
|
||||
const entries = json.data.repository.object.entries
|
||||
const directories = entries.filter(entry => entry.type === 'tree')
|
||||
const versionDirs = directories.filter(dir =>
|
||||
dir.object.entries.some(
|
||||
file => file.type === 'blob' && file.name === `${name}.yaml`,
|
||||
),
|
||||
)
|
||||
const versions = versionDirs.map(dir => dir.name)
|
||||
const version = latest(versions)
|
||||
|
||||
if (version == null) {
|
||||
throw new InvalidParameter({
|
||||
prettyMessage: 'no versions found',
|
||||
})
|
||||
}
|
||||
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
343
services/winget/winget-version.tester.js
Normal file
343
services/winget/winget-version.tester.js
Normal file
@ -0,0 +1,343 @@
|
||||
import { isVPlusDottedVersionNClauses } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
// basic test
|
||||
t.create('gets the package version of WSL')
|
||||
.get('/Microsoft.WSL.json')
|
||||
.expectBadge({ label: 'winget', message: isVPlusDottedVersionNClauses })
|
||||
|
||||
// test more than one dots
|
||||
t.create('gets the package version of .NET 8')
|
||||
.get('/Microsoft.DotNet.SDK.8.json')
|
||||
.expectBadge({ label: 'winget', message: isVPlusDottedVersionNClauses })
|
||||
|
||||
// test sort based on dotted version order instead of ASCII
|
||||
t.create('gets the latest version')
|
||||
.intercept(nock =>
|
||||
nock('https://api.github.com/')
|
||||
.post('/graphql')
|
||||
.reply(200, {
|
||||
data: {
|
||||
repository: {
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.1001.389.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.1101.416.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.1201.442.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.137.141.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.200.170.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.503.261.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.601.285.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.601.297.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.701.323.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '0.801.344.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Microsoft.DevHome.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.get('/Microsoft.DevHome.json')
|
||||
.expectBadge({ label: 'winget', message: 'v0.1201.442.0' })
|
||||
|
||||
// Both 'Some.Package' and 'Some.Package.Sub' are present in the response.
|
||||
// We should ignore 'Some.Package.Sub' in response to 'Some.Package' request.
|
||||
// In this test case, Canonical.Ubuntu.2404 is present, but it should not be treated as Canonical.Ubuntu version 2404.
|
||||
t.create('do not pick sub-package as version')
|
||||
.intercept(nock =>
|
||||
nock('https://api.github.com/')
|
||||
.post('/graphql')
|
||||
.reply(200, {
|
||||
data: {
|
||||
repository: {
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: '.validation',
|
||||
object: {},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '1804',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'tree',
|
||||
name: '1804.6.4.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2004',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2004.6.16.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2204.1.8.0',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Canonical.Ubuntu.installer.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Canonical.Ubuntu.locale.en-US.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Canonical.Ubuntu.locale.zh-CN.yaml',
|
||||
},
|
||||
{
|
||||
type: 'blob',
|
||||
name: 'Canonical.Ubuntu.yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2204',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: '.validation',
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2204.0.10.0',
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2204.2.47.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2404',
|
||||
object: {
|
||||
entries: [
|
||||
{
|
||||
type: 'blob',
|
||||
name: '.validation',
|
||||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: '2404.0.5.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.get('/Canonical.Ubuntu.json')
|
||||
.expectBadge({ label: 'winget', message: 'v2204.1.8.0' })
|
Loading…
x
Reference in New Issue
Block a user