Skip to content

Commit

Permalink
endpoints/cli: Support PEP 440/508 version range constraints for down…
Browse files Browse the repository at this point in the history
…loads

This allows URLs like:

    https://nextstrain.org/cli/download/>=7.4.0/standalone-x86_64-unknown-linux-gnu.tar.gz

which will let us use the standalone installer, e.g.

    curl -fsSL --proto '=https' https://nextstrain.org/cli/installer/linux | bash -s '>=7.4.0'

in automated contexts where we want to be able to declare constraints
like lower version bounds or incompatible versions.

Note that the standalone installer on macOS on aarch64 hardware does
very rudimentary version comparison¹ to decide if the requested version
is older or newer than the first release with actual aarch64 support
(8.2.0), and it won't compare version range constraints correctly:
they'll always be considered greater than (newer/later) than 8.2.0.  But
I expect this to be fine in practice and not matter for actual usage.

We depend on an older version of @renovatebot/pep440 (<3) because >=3
adds a dependency on Node >=18 and this codebase is still on Node 16.  I
reviewed the changelog for newer versions of the package and nothing
else substantial seems to have changed anyhow.

Related-to: <nextstrain/.github#55>

¹ <nextstrain/cli#358>
  <https://github.com/nextstrain/cli/blob/af976b06/bin/standalone-installer-unix#L146-L154>
  • Loading branch information
tsibley committed Feb 12, 2024
1 parent 8c3a45a commit a7f4085
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 6 deletions.
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@aws-sdk/s3-request-presigner": "^3.431.0",
"@keyv/redis": "^2.8.0",
"@keyv/sqlite": "^3.6.6",
"@renovatebot/pep440": "^2.1.20",
"@smithy/node-http-handler": "^2.1.8",
"argparse": "^1.0.10",
"chalk": "^2.4.1",
Expand All @@ -44,6 +45,7 @@
"express-static-gzip": "^0.2.2",
"heroku-ssl-redirect": "0.0.4",
"http-errors": "^1.8.0",
"http-link-header": "^1.1.1",
"ioredis": "^4.14.1",
"jose": "npm:jose-node-cjs-runtime@^3.11.3",
"js-yaml": "^4.0.0",
Expand Down
50 changes: 44 additions & 6 deletions src/endpoints/cli.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import LinkHeader from 'http-link-header';
import jszip from 'jszip';
import mime from 'mime';
import pep440 from '@renovatebot/pep440';
import { pipeline } from 'stream/promises';
import { BadRequest, InternalServerError, NotFound, ServiceUnavailable } from '../httpErrors.js';
import { fetch } from '../fetch.js';
import { uri } from '../templateLiterals.js';
import { map } from '../utils/iterators.js';


const authorization = process.env.GITHUB_TOKEN
Expand All @@ -16,14 +19,49 @@ export async function download(req, res) {
const assetSuffix = req.params.assetSuffix;
if (!version || !assetSuffix) throw new BadRequest();

const endpoint = version === "latest"
? "https://api.github.com/repos/nextstrain/cli/releases/latest"
: uri`https://api.github.com/repos/nextstrain/cli/releases/tags/${version}`;
/* Convert "latest" into a valid version constraint (albeit one with no
* constraints).
*
* Convert an (assumed) exact version into a valid version constraint using
* the exact equality operator.
*/
const constraint =
version === "latest" ? "" :
!pep440.validRange(version) ? `==${version}` :
version ;

/* Fetch all releases, 100 per request. This will remain only a single
* request for a while as we currently only have 24 releases to GitHub and
* the entire project has only had 88 releases (to PyPI) in ~6 years, an
* average of roughly 15 releases per year. Additionally, our fetch()
* caching will greatly reduce actual network traffic and latency. Taken
* together, I have no qualms putting this chained set of fetches in the path
* of every response from this download handler (at least for now).
* -trs, 12 Feb 2024
*/
const releases = new Map(await map(
(async function* () {
let nextPage = "https://api.github.com/repos/nextstrain/cli/releases?per_page=100";

while (nextPage) {
const response = await fetch(nextPage, {headers: {authorization}});
assertStatusOk(response);

const response = await fetch(endpoint, {headers: {authorization}});
assertStatusOk(response);
yield* await response.json();

nextPage = LinkHeader.parse(response.headers.get("Link") ?? "").rel("next")[0];
}
})(),
r => [r.tag_name, r]
));

const maxSatisfyingVersion = pep440.maxSatisfying([...releases.keys()], constraint);

if (!maxSatisfyingVersion) {
throw new NotFound(`No release version matches requested PEP 440/508 version constraint(s): ${constraint || "[none]"} ("${version}")`);
}

const release = await response.json();
const release = releases.get(maxSatisfyingVersion);
const assetName = `nextstrain-cli-${release.tag_name}-${assetSuffix}`;
const asset = release.assets.find(a => a.name === assetName);

Expand Down

0 comments on commit a7f4085

Please sign in to comment.