diff --git a/.github/workflows/_mirror-distribution.yml b/.github/workflows/_mirror-distribution.yml new file mode 100644 index 00000000..ae2a8d59 --- /dev/null +++ b/.github/workflows/_mirror-distribution.yml @@ -0,0 +1,93 @@ +name: _get-unmirrored-distributions + +on: + workflow_call: + inputs: + name: + type: string + required: true + distribution: + type: string + required: true + secrets: + AWS_S3_BUCKET: + required: true + AWS_ACCESS_KEY_ID: + required: true + AWS_SECRET_ACCESS_KEY: + required: true + +jobs: + get-unmirrored-versions: + name: Get unmirrored versions - ${{ inputs.name }} + runs-on: ubuntu-22.04 + outputs: + versions: ${{ steps.get-unmirrored-versions.outputs.versions }} + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Update Rust toolchain + run: rustup update + + - name: Rust Cache + uses: Swatinem/rust-cache@v2.5.1 + + - name: Get unmirrored versions + id: get-unmirrored-versions + env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + run: echo "versions=$(cargo run --bin list_unmirrored_versions ${{ inputs.distribution }})" >> $GITHUB_OUTPUT + + mirror-node-distribution: + if: inputs.distribution == 'node' + name: Mirror Distribution - ${{ inputs.name }} - ${{ matrix.version }} ${{ matrix.platform }} + needs: [get-unmirrored-versions] + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + platform: [ "linux-x64" ] + version: ${{ fromJson(needs.get-unmirrored-versions.outputs.versions) }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Download and verify distribution + run: common/bin/download-verify-node "${{ matrix.version }}" "${{ matrix.platform }}" + + - name: Upload Node.js distribution to Nodebin S3 bucket + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: us-east-1 + run: > + aws s3 cp + "node-v${{ matrix.version }}-${{ matrix.platform }}.tar.gz" + "s3://${{ secrets.AWS_S3_BUCKET }}/node/release/${{ matrix.platform}}/node-v${{ matrix.version }}-${{ matrix.platform }}.tar.gz" + + mirror-npm-package-distribution: + if: inputs.distribution != 'node' + name: Mirror Distribution - ${{ inputs.name }} - ${{ matrix.version }} ${{ matrix.platform }} + runs-on: ubuntu-22.04 + needs: [get-unmirrored-versions] + strategy: + fail-fast: false + matrix: + version: ${{ fromJson(needs.get-unmirrored-yarn-versions.outputs.versions) }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Download and verify npm package + run: common/bin/download-verify-npm-package ${{ inputs.distribution }} "${{ matrix.version }}" + + - name: Upload distribution to S3 bucket + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: us-east-1 + run: > + aws s3 cp + "${{ inputs.distribution }}-v${{ matrix.version }}.tar.gz" + "s3://${{ secrets.AWS_S3_BUCKET }}/${{ inputs.distribution }}/release/${{ inputs.distribution }}-v${{ matrix.version }}.tar.gz" diff --git a/.github/workflows/_update-inventory.yml b/.github/workflows/_update-inventory.yml new file mode 100644 index 00000000..5915a495 --- /dev/null +++ b/.github/workflows/_update-inventory.yml @@ -0,0 +1,81 @@ +name: _update-inventory + +env: + CARGO_TERM_COLOR: always + +on: + workflow_call: + inputs: + name: + type: string + required: true + distribution: + type: string + required: true + buildpack_id: + type: string + required: true + buildpack_path: + type: string + required: true + +jobs: + update-inventory: + name: Update Inventory - ${{ inputs.name }} + runs-on: pub-hk-ubuntu-22.04-small + steps: + - uses: heroku/use-app-token-action@main + id: generate-token + with: + app_id: ${{ vars.LINGUIST_GH_APP_ID }} + private_key: ${{ secrets.LINGUIST_GH_PRIVATE_KEY }} + + - name: Checkout Repo + uses: actions/checkout@v3 + with: + token: ${{ steps.generate-token.outputs.app_token }} + + - name: Configure git + run: | + git config --global user.name ${{ vars.LINGUIST_GH_APP_USERNAME }} + git config --global user.email ${{ vars.LINGUIST_GH_APP_EMAIL }} + + - name: Update Rust toolchain + run: rustup update + + - name: Rust cache + uses: Swatinem/rust-cache@v2.6.0 + + - name: Set Diff Message + id: set-diff-msg + run: | + delimiter="$(openssl rand -hex 8)" + { + echo "msg<<${delimiter}" + #cargo run --bin diff_versions ${{ inputs.distribution }} ${{ inputs.buildpack_path }}/inventory.toml + echo "Added Node.js version 20.3.1.\nAdded Node.js version x.y.z\nRemoved Node.js version a.b.c" + echo "${delimiter}" + } >> $GITHUB_OUTPUT + + - name: Rebuild Inventory + run: cargo run --bin generate_inventory ${{ inputs.distribution }} > ${{ inputs.buildpack_path }}/inventory.toml + + - name: Update Changelog + run: echo "${{ steps.set-diff-msg.outputs.msg }}" | xargs -r -I '{}' perl -i -p -e + ${{ inputs.buildpack_path }}/CHANGELOG.md + + - name: Create Pull Request + id: pr + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ steps.generate-token.outputs.app_token }} + title: Update Inventory - ${{ inputs.name }} + branch: update-${{ inputs.distribution }}-inventory + commit-message: "Update Inventory for ${{ inputs.buildpack_id }}\n\n${{ steps.set-diff-msg.outputs.msg }}" + body: "Automated pull-request to update ${{ inputs.buildpack_id }} inventory:\n\n${{ steps.set-diff-msg.outputs.msg }}" + + - name: Configure PR + if: steps.pr.outputs.pull-request-operation == 'created' + env: + GH_TOKEN: ${{ steps.generate-token.outputs.app_token }} + run: gh pr merge --squash --auto "${{ steps.pr.outputs.pull-request-number }}" diff --git a/.github/workflows/inventory.yml b/.github/workflows/inventory.yml index 31fbf916..d4fde9df 100644 --- a/.github/workflows/inventory.yml +++ b/.github/workflows/inventory.yml @@ -1,4 +1,5 @@ name: Update Inventory + on: workflow_dispatch: schedule: @@ -6,79 +7,25 @@ on: jobs: update-nodejs-inventory: - name: Update Node.js Engine Inventory - runs-on: pub-hk-ubuntu-22.04-small - steps: - - name: Checkout Repo - uses: actions/checkout@v3 - - id: install-rust-toolchain - name: Install Rust Toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - id: set-diff-msg - name: Set Diff Message - run: echo "::set-output name=msg::$(cargo run --bin diff_versions node buildpacks/nodejs-engine/inventory.toml)" - - name: Rebuild Inventory - run: "cargo run --bin generate_inventory node > buildpacks/nodejs-engine/inventory.toml" - - name: Update Changelog - run: echo "${{ steps.set-diff-msg.outputs.msg }}" | xargs -r -I '{}' perl -i -p -e 's/\[Unreleased\]\s+/[Unreleased]\n\n{}/' buildpacks/nodejs-engine/CHANGELOG.md - - uses: heroku/use-app-token-action@main - id: generate-token - with: - app_id: ${{ vars.LINGUIST_GH_APP_ID }} - private_key: ${{ secrets.LINGUIST_GH_PRIVATE_KEY }} - - name: Create Pull Request - id: pr - uses: peter-evans/create-pull-request@v5 - with: - token: ${{ steps.generate-token.outputs.app_token }} - title: "Update Node.js Engine Inventory" - commit-message: "Update Inventory for heroku/nodejs-engine\n\n${{ steps.set-diff-msg.outputs.msg }}" - branch: update-nodejs-inventory - labels: "automation" - body: "Automated pull-request to update heroku/nodejs-engine inventory:\n\n${{ steps.set-diff-msg.outputs.msg }}" - - name: Configure PR - if: steps.pr.outputs.pull-request-operation == 'created' - run: gh pr merge --squash --auto "${{ steps.pr.outputs.pull-request-number }}" - env: - GH_TOKEN: ${{ steps.generate-token.outputs.app_token }} - + uses: ./_update-inventory.yml + with: + name: Node.js Engine + distribution: node + buildpack_id: heroku/nodejs-engine + buildpack_path: buildpacks/nodejs-engine + update-yarn-inventory: - name: Update Node.js Yarn Inventory - runs-on: pub-hk-ubuntu-22.04-small - steps: - - name: Checkout Repo - uses: actions/checkout@v3 - - id: install-rust-toolchain - name: Install Rust Toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - id: set-diff-msg - name: Set Diff Message - run: echo "::set-output name=msg::$(cargo run --bin diff_versions yarn buildpacks/nodejs-yarn/inventory.toml)" - - name: Rebuild Inventory - run: "cargo run --bin generate_inventory yarn > buildpacks/nodejs-yarn/inventory.toml" - - name: Update Changelog - run: echo "${{ steps.set-diff-msg.outputs.msg }}" | xargs -r -I '{}' perl -i -p -e 's/\[Unreleased\]\s+/[Unreleased]\n\n{}/' buildpacks/nodejs-yarn/CHANGELOG.md - - uses: heroku/use-app-token-action@main - id: generate-token - with: - app_id: ${{ vars.LINGUIST_GH_APP_ID }} - private_key: ${{ secrets.LINGUIST_GH_PRIVATE_KEY }} - - name: Create Pull Request - id: pr - uses: peter-evans/create-pull-request@v5 - with: - token: ${{ steps.generate-token.outputs.app_token }} - title: "Update Node.js Yarn Inventory" - commit-message: "Update Inventory for heroku/nodejs-yarn\n\n${{ steps.set-diff-msg.outputs.msg }}" - branch: update-yarn-inventory - labels: "automation" - body: "Automated pull-request to update heroku/nodejs-yarn inventory:\n\n${{ steps.set-diff-msg.outputs.msg }}" - - name: Configure PR - if: steps.pr.outputs.pull-request-operation == 'created' - run: gh pr merge --squash --auto "${{ steps.pr.outputs.pull-request-number }}" - env: - GH_TOKEN: ${{ steps.generate-token.outputs.app_token }} + uses: ./_update-inventory.yml + with: + name: Yarn + distribution: yarn + buildpack_id: heroku/nodejs-yarn + buildpack_path: buildpacks/nodejs-yarn + + update-npm-inventory: + uses: ./_update-inventory.yml + with: + name: NPM + distribution: npm + buildpack_id: heroku/nodejs-npm-engine + buildpack_path: buildpacks/nodejs-npm-engine diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 39a1af88..653f5ffd 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -5,89 +5,23 @@ on: - cron: '00 1 * * 1-5' jobs: - get-unmirrored-node-versions: - name: Get unmirrored Node.js versions - runs-on: ubuntu-22.04 - outputs: - versions: ${{ steps.get-unmirrored-versions.outputs.versions }} - steps: - - name: Checkout Repo - uses: actions/checkout@v3 - - name: Update Rust toolchain - run: rustup update - - name: Rust Cache - uses: Swatinem/rust-cache@v2.5.1 - - id: get-unmirrored-versions - name: Get unmirrored Node.js versions - env: - AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} - run: echo "versions=$(cargo run --bin list_unmirrored_versions node)" >> $GITHUB_OUTPUT - - get-unmirrored-yarn-versions: - name: Get unmirrored Yarn versions - runs-on: ubuntu-22.04 - outputs: - versions: ${{ steps.get-unmirrored-versions.outputs.versions }} - steps: - - name: Checkout Repo - uses: actions/checkout@v3 - - name: Update Rust toolchain - run: rustup update - - name: Rust Cache - uses: Swatinem/rust-cache@v2.5.1 - - id: get-unmirrored-versions - name: Get unmirrored Yarn versions - env: - AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} - run: echo "versions=$(cargo run --bin list_unmirrored_versions yarn)" >> $GITHUB_OUTPUT - - mirror-node-distribution: - name: Mirror Node.js Distribution ${{ matrix.version }} ${{ matrix.platform }} - runs-on: ubuntu-22.04 - needs: get-unmirrored-node-versions - strategy: - fail-fast: false - matrix: - platform: ["linux-x64"] - version: ${{ fromJson(needs.get-unmirrored-node-versions.outputs.versions) }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Download and Verify Node.js distribution - run: common/bin/download-verify-node "${{ matrix.version }}" "${{ matrix.platform }}" - - - name: Upload Node.js distribution to Nodebin S3 bucket - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: us-east-1 - run: > - aws s3 cp - "node-v${{ matrix.version }}-${{ matrix.platform }}.tar.gz" - "s3://${{ secrets.AWS_S3_BUCKET }}/node/release/${{ matrix.platform}}/node-v${{ matrix.version }}-${{ matrix.platform }}.tar.gz" - - mirror-yarn-distribution: - name: Mirror Yarn Distribution ${{ matrix.version }} - runs-on: ubuntu-22.04 - needs: get-unmirrored-yarn-versions - strategy: - fail-fast: false - matrix: - version: ${{ fromJson(needs.get-unmirrored-yarn-versions.outputs.versions) }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Download and verify yarn distribution - run: common/bin/download-verify-yarn "${{ matrix.version }}" - - - name: Upload Yarn distribution to S3 bucket - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: us-east-1 - run: > - aws s3 cp - "yarn-v${{ matrix.version }}.tar.gz" - "s3://${{ secrets.AWS_S3_BUCKET }}/yarn/release/yarn-v${{ matrix.version }}.tar.gz" + mirror-node: + uses: ./_mirror-distribution.yml + with: + name: Node.js + distribution: node + secrets: inherit + + mirror-yarn: + uses: ./_mirror-distribution.yml + with: + name: Yarn + distribution: yarn + secrets: inherit + + mirror-npm: + uses: ./_mirror-distribution.yml + with: + name: NPM + distribution: npm + secrets: inherit diff --git a/buildpacks/nodejs-npm-engine/inventory.toml b/buildpacks/nodejs-npm-engine/inventory.toml new file mode 100644 index 00000000..e69de29b diff --git a/common/bin/download-verify-yarn b/common/bin/download-verify-npm-package similarity index 71% rename from common/bin/download-verify-yarn rename to common/bin/download-verify-npm-package index 6155f2a8..21cd7fdb 100755 --- a/common/bin/download-verify-yarn +++ b/common/bin/download-verify-npm-package @@ -7,26 +7,33 @@ set -o pipefail set -e -package_version=$1 +package_name=$1 +if [ "npm" != "${package_name}" ] && [ "yarn" != "${package_name}" ]; then + echo "Unrecognized distribution - ${package_name}" +fi + +package_version=$2 if [ -z "$package_version" ]; then - echo "Yarn package version not provided. Should be something like '1.22.19'" + echo "Version not provided. Should be something like '1.22.19'" exit 1 fi -# Yarn 2+ (aka: "berry") is hosted under a different npm package. -major_version=$(echo "$package_version" | cut -d "." -f 1) -package_name=$([ "$major_version" -ge 2 ] && echo "@yarnpkg/cli-dist" || echo "yarn") +if [ "yarn" = "${package_name}" ]; then + # Yarn 2+ (aka: "berry") is hosted under a different npm package. + major_version=$(echo "$package_version" | cut -d "." -f 1) + package_name=$([ "$major_version" -ge 2 ] && echo "@yarnpkg/cli-dist" || echo "yarn") +fi -echo "Downloading yarn tarball..." >&2 +echo "Downloading ${package_name} tarball..." >&2 url=$(curl -sSf "https://registry.npmjs.com/${package_name}/${package_version}" | jq -r '.dist.tarball') -curl -sSf -o "./yarn-v${package_version}.tar.gz" "${url}" +curl -sSf -o "./${package_name}-v${package_version}.tar.gz" "${url}" # Check the file's sha against npm's published sha. This section assumes all # packages are published with sha512. That was true at the time of writing, # but if npmjs.org starts using additional checksum algorithms, this section # will need to be changed. -echo "Checking yarn tarball integrity..." >&2 -shasum=$(shasum -b -a 512 yarn-v"${package_version}".tar.gz | awk '{ print $1 }' | xxd -r -p | base64 | tr -d "\n") +echo "Checking ${package_name} tarball integrity..." >&2 +shasum=$(shasum -b -a 512 ${package_name}-v"${package_version}".tar.gz | awk '{ print $1 }' | xxd -r -p | base64 | tr -d "\n") actual_integrity="sha512-${shasum}" published_integrity=$(curl -sSf "https://registry.npmjs.com/${package_name}/${package_version}" | jq -r '.dist.integrity') if [ "$actual_integrity" != "$published_integrity" ]; then diff --git a/common/nodejs-utils/src/bin/diff_versions.rs b/common/nodejs-utils/src/bin/diff_versions.rs index 12f11344..a23f576a 100644 --- a/common/nodejs-utils/src/bin/diff_versions.rs +++ b/common/nodejs-utils/src/bin/diff_versions.rs @@ -60,5 +60,7 @@ fn main() { } fn print_usage() { - eprintln!("$ AWS_S3_BUCKET=heroku-nodebin diff_versions path/to/inventory.toml"); + eprintln!( + "$ AWS_S3_BUCKET=heroku-nodebin diff_versions path/to/inventory.toml" + ); } diff --git a/common/nodejs-utils/src/bin/generate_inventory.rs b/common/nodejs-utils/src/bin/generate_inventory.rs index a6f68a9b..52c3276d 100644 --- a/common/nodejs-utils/src/bin/generate_inventory.rs +++ b/common/nodejs-utils/src/bin/generate_inventory.rs @@ -34,5 +34,5 @@ fn main() { } fn print_usage() { - eprintln!("Usage: $ AWS_S3_BUCKET=heroku-nodebin generate_inventory "); + eprintln!("Usage: $ AWS_S3_BUCKET=heroku-nodebin generate_inventory "); } diff --git a/common/nodejs-utils/src/bin/list_unmirrored_versions.rs b/common/nodejs-utils/src/bin/list_unmirrored_versions.rs index f94d3b6c..f18d8cca 100644 --- a/common/nodejs-utils/src/bin/list_unmirrored_versions.rs +++ b/common/nodejs-utils/src/bin/list_unmirrored_versions.rs @@ -64,5 +64,5 @@ fn main() { } fn print_usage() { - eprintln!("Usage: $ AWS_S3_BUCKET=heroku-nodebin list_unmirrored_versions "); + eprintln!("Usage: $ AWS_S3_BUCKET=heroku-nodebin list_unmirrored_versions "); } diff --git a/common/nodejs-utils/src/distribution.rs b/common/nodejs-utils/src/distribution.rs index 6104213c..570d9a02 100644 --- a/common/nodejs-utils/src/distribution.rs +++ b/common/nodejs-utils/src/distribution.rs @@ -16,6 +16,7 @@ pub const DEFAULT_REGION: &str = "us-east-1"; pub enum Distribution { Yarn, Node, + Npm, } impl FromStr for Distribution { @@ -24,6 +25,7 @@ impl FromStr for Distribution { match s { "Node.js" | "node" => Ok(Self::Node), "Yarn" | "yarn" => Ok(Self::Yarn), + "Npm" | "npm" => Ok(Self::Npm), other => Err(anyhow!("Unknown distribution: {other}")), } } @@ -34,6 +36,7 @@ impl fmt::Display for Distribution { match self { Self::Node => write!(f, "Node.js"), Self::Yarn => write!(f, "Yarn"), + Self::Npm => write!(f, "npm"), } } } @@ -48,6 +51,7 @@ impl Distribution { match self { Self::Node => list_upstream_node_versions(), Self::Yarn => list_upstream_yarn_versions(), + Self::Npm => list_upstream_npm_versions(), } } @@ -85,6 +89,7 @@ impl Distribution { match self { Self::Node => "node", Self::Yarn => "yarn", + Self::Npm => "npm", } } @@ -93,6 +98,7 @@ impl Distribution { Requirement::parse(match self { Self::Node => ">=16", Self::Yarn => ">=1.22 || >=4.0.0-rc.35", + Self::Npm => ">=8", }) .map_err(|e| anyhow!("{e}")) } @@ -101,6 +107,7 @@ impl Distribution { Regex::new(match self { Self::Node => r"node/(?P\w+)/(?P[\w-]+)/node-v(?P\d+\.\d+\.\d+)[\w-]+\.tar\.gz", Self::Yarn => r"yarn/(?P\w+)/yarn-v(?P\d+\.\d+\.\d+(-[\w\.]+)?)\.tar\.gz", + Self::Npm => r"npm/(?P\w+)/npm-v(?P\d+\.\d+\.\d+(-[\w\.]+)?)\.tar\.gz" }).map_err(|e| anyhow!("Mirrored release regex error: {e}")) } } @@ -126,6 +133,15 @@ fn list_upstream_yarn_versions() -> anyhow::Result { Ok(vset) } +fn list_upstream_npm_versions() -> anyhow::Result { + npmjs_org::list_releases("npm").map(|releases| { + releases + .into_iter() + .map(|release| release.version) + .collect() + }) +} + #[cfg(test)] mod tests { use super::*; @@ -157,6 +173,19 @@ mod tests { assert_eq!(&expected_version, actual_version); } + #[test] + fn upstream_versions_npm() { + let dist = Distribution::Npm {}; + let expected_version = Version::parse("9.7.2").expect("Expected to parse a valid version"); + let versions = dist + .upstream_versions() + .expect("Expected to list upstream remote versions, but got an error"); + let actual_version = versions + .get(&expected_version) + .expect("Expected to find a matching version"); + assert_eq!(&expected_version, actual_version); + } + #[test] fn mirrored_versions_node() { let dist = Distribution::Node {}; diff --git a/common/nodejs-utils/src/npmjs_org.rs b/common/nodejs-utils/src/npmjs_org.rs index d012b41f..53562f97 100644 --- a/common/nodejs-utils/src/npmjs_org.rs +++ b/common/nodejs-utils/src/npmjs_org.rs @@ -18,8 +18,8 @@ pub(crate) struct NpmRelease { pub(crate) fn list_releases(package: &str) -> anyhow::Result> { ureq::get(&format!("{NPMJS_ORG_HOST}/{package}")) .call() - .map_err(|e| anyhow!("Couldn't fetch npmjs.org package info: {e}"))? + .map_err(|e| anyhow!("Couldn't fetch npmjs registry release list from for {package}: {e}"))? .into_json::() - .map_err(|e| anyhow!("Couldn't serialize nodejs.org release list from package: {e}")) + .map_err(|e| anyhow!("Couldn't serialize npmjs registry release list for {package}: {e}")) .map(|rel| rel.versions.into_values().collect()) } diff --git a/common/nodejs-utils/src/s3.rs b/common/nodejs-utils/src/s3.rs index 8732d71a..d6ff96b4 100644 --- a/common/nodejs-utils/src/s3.rs +++ b/common/nodejs-utils/src/s3.rs @@ -8,6 +8,7 @@ use url::Url; #[allow(dead_code)] pub(crate) struct Content { // Examples of keys: + // * npm/release/npm-v9.7.2.tar.gz // * yarn/release/yarn-v0.16.0.tar.gz // * node/release/darwin-x64/node-v0.10.0-darwin-x64.tar.gz pub(crate) key: String,