Compare commits

...

9 Commits

Author SHA1 Message Date
Taiki Endo
06203676c6 Release 2.69.6 2026-03-22 02:50:18 +09:00
Taiki Endo
c35d18270e Support signature verification for mise and syft 2026-03-22 01:51:55 +09:00
Taiki Endo
525387f706 codegen: Clean up error handling and move some checks to appropriate
place

Unless error handling is done in particular care, in private tools this
approach tends to make debugging easier when failed.
2026-03-22 00:24:42 +09:00
Taiki Endo
7a6eff0bac Update cargo-binstall@latest to 1.17.8 2026-03-21 09:24:46 +00:00
Taiki Endo
458413b553 Update tombi@latest to 0.9.9 2026-03-21 05:38:52 +00:00
Taiki Endo
b988c18e3d Release 2.69.5 2026-03-21 12:38:29 +09:00
Taiki Endo
5fe6797db0 Update cargo-nextest@latest to 0.9.132 2026-03-21 00:46:56 +00:00
Taiki Endo
a7e592b247 Release 2.69.4 2026-03-21 06:23:44 +09:00
Taiki Endo
a4d6c73c76 manifest-schema: Update changelog 2026-03-21 06:19:53 +09:00
14 changed files with 473 additions and 117 deletions

View File

@@ -20,6 +20,7 @@ libicu
linkcheck
mdbook
microdnf
minisig
mirrorlist
nextest
pluginconf
@@ -30,6 +31,8 @@ rclone
rdme
rootfs
sccache
SHASUMS
sigstore
syft
tombi
udeps

View File

@@ -10,6 +10,24 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
## [Unreleased]
## [2.69.6] - 2026-03-21
- Support signature verification for `mise` and `syft`. ([#1611](https://github.com/taiki-e/install-action/pull/1611))
- Update `mise@latest` to 2026.3.10.
- Update `knope@latest` to 0.22.4.
- Update `cargo-binstall@latest` to 1.17.8.
- Update `tombi@latest` to 0.9.9.
## [2.69.5] - 2026-03-21
- Update `cargo-nextest@latest` to 0.9.132.
## [2.69.4] - 2026-03-20
- Support artifact attestations verification for `biome`, `cargo-cyclonedx`, `cargo-hack`, `cargo-llvm-cov`, `cargo-minimal-versions`, `cargo-no-dev-deps`, `martin`, `parse-changelog`, `parse-dockerfile`, `prek`, `uv`, `wasmtime`, `zizmor`, and `zola`. ([#1606](https://github.com/taiki-e/install-action/pull/1606))
- Update `biome@latest` to 2.4.8.
@@ -5981,7 +5999,10 @@ Note: This release is considered a breaking change because installing on version
Initial release
[Unreleased]: https://github.com/taiki-e/install-action/compare/v2.69.3...HEAD
[Unreleased]: https://github.com/taiki-e/install-action/compare/v2.69.6...HEAD
[2.69.6]: https://github.com/taiki-e/install-action/compare/v2.69.5...v2.69.6
[2.69.5]: https://github.com/taiki-e/install-action/compare/v2.69.4...v2.69.5
[2.69.4]: https://github.com/taiki-e/install-action/compare/v2.69.3...v2.69.4
[2.69.3]: https://github.com/taiki-e/install-action/compare/v2.69.2...v2.69.3
[2.69.2]: https://github.com/taiki-e/install-action/compare/v2.69.1...v2.69.2
[2.69.1]: https://github.com/taiki-e/install-action/compare/v2.69.0...v2.69.1

View File

@@ -22,32 +22,32 @@
},
"license_markdown": "[GPL-3.0](https://github.com/cargo-bins/cargo-binstall/blob/HEAD/crates/bin/LICENSE)",
"latest": {
"version": "1.17.7"
"version": "1.17.8"
},
"1.17.7": {
"1.17.8": {
"x86_64_linux_musl": {
"etag": "0x8DE7C5AC0E4497E",
"hash": "29b5ecfb6e03c2511a617c77d312b06df0c54717644fbfda3d465ec8240532f0"
"etag": "0x8DE8715DB8A1417",
"hash": "1da1ef72448db667cc4ae6d48e37451087602c8c07dc61782a4a5e538303e015"
},
"x86_64_macos": {
"etag": "0x8DE7C5AC5E5BC1B",
"hash": "aa7174fb938e668dea4b4c3d22fe6cefed97642cc3a7a419ba96d63d63fd729b"
"etag": "0x8DE8715E03D9720",
"hash": "db353e01b582c97382178db9b4dfe22d81109782e480a38f3db953e62f569952"
},
"x86_64_windows": {
"etag": "0x8DE7C5AC441ACDB",
"hash": "c5cb2444ee04480502a8ac73d96abd9f97af8300ec04ea1c1f2a9e959c02e4d6"
"etag": "0x8DE8715DEAA171B",
"hash": "fef07560d4e391812091bb30c6ed1bd5289f74403a0c947b47b8a8c7a597b51b"
},
"aarch64_linux_musl": {
"etag": "0x8DE7C5ACC95E091",
"hash": "b0658b0a7f0959bc1dbb4ab665931c31c7dd1109ff01cb8772af17dfdc52a9af"
"etag": "0x8DE8715E6784BD0",
"hash": "81d6245bd1a7a89e914d29af81d82280540e94927e61492a0fc359820cd97abb"
},
"aarch64_macos": {
"etag": "0x8DE7C5AD2545078",
"hash": "1ad3c0c56fa3970634cce5009ed0ce61b943515f9115f8e480fd0e41d8d89085"
"etag": "0x8DE8715EBEC4A3F",
"hash": "af87346fdb186f0a2333bc0a30cfddd6faa98b31145ef1bb19c284aedea65972"
},
"aarch64_windows": {
"etag": "0x8DE7C5AD068DA1E",
"hash": "e876543c9aad23968d1123c0d959309937894bbfd267bb0878109fb253217878"
"etag": "0x8DE8715EA179DDA",
"hash": "2270a5a7a8b3e85bd5fe32ac3fbd48cfd32d6f468a8c35499af8b65b806d271d"
}
}
}

View File

@@ -28,10 +28,45 @@
},
"license_markdown": "[Apache-2.0](https://github.com/nextest-rs/nextest/blob/main/LICENSE-APACHE) OR [MIT](https://github.com/nextest-rs/nextest/blob/main/LICENSE-MIT)",
"latest": {
"version": "0.9.131"
"version": "0.9.132"
},
"0.9": {
"version": "0.9.131"
"version": "0.9.132"
},
"0.9.132": {
"previous_stable_version": "0.9.131",
"x86_64_linux_gnu": {
"etag": "0x8DE86DAD99145E2",
"hash": "e22f14ecaff5519dbfe521e8717d64e9989648bddc23eb1f71bb0053518a52e7"
},
"x86_64_linux_musl": {
"etag": "0x8DE86DB277E6673",
"hash": "5e93f3b4244e2f8bffe5818b2a7cbd6a59f32d262dcb6017905e345cc74227af"
},
"x86_64_macos": {
"etag": "0x8DE86DB84D52D01",
"hash": "6ce5c844ae3cdac3f6f42fd86bf71a8bf99aecd76bab9f6743c808223d31fad1"
},
"x86_64_windows": {
"etag": "0x8DE86DBA597EDA0",
"hash": "6f934574f621613d7d759547401a04619b3e1ebe1f4e8f624880197d566fa6ad"
},
"aarch64_linux_gnu": {
"etag": "0x8DE86DBC5227BB5",
"hash": "592db3fa3d3ee62f109dc149554811eb8ecdc68e1514be5af79986b1560e2e0d"
},
"aarch64_linux_musl": {
"etag": "0x8DE86DAF803192A",
"hash": "2497ddfd2a0c6805f7c5514ff680c6ca9c1ffc5b9ab30af43e85830b768c129a"
},
"aarch64_windows": {
"etag": "0x8DE86DBA23732E6",
"hash": "3c519d128909fba6829a318476bcbd9777d97ec066b4d973a3f1e04da049ef0b"
},
"riscv64_linux_gnu": {
"etag": "0x8DE86DAD1A09377",
"hash": "82d1f0c4b7733e0a852e2e8b3f1a3f65b89e7ffd165121127ac356f0c9f7b5d2"
}
},
"0.9.131": {
"previous_stable_version": "0.9.130",

36
manifests/knope.json generated
View File

@@ -3,10 +3,42 @@
"template": null,
"license_markdown": "[MIT](https://github.com/knope-dev/knope/blob/main/LICENSE)",
"latest": {
"version": "0.22.3"
"version": "0.22.4"
},
"0.22": {
"version": "0.22.3"
"version": "0.22.4"
},
"0.22.4": {
"x86_64_linux_musl": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-x86_64-unknown-linux-musl.tgz",
"etag": "0x8DE8761D8F513DE",
"hash": "45a74925ae9f4c9c2c33b51992ae50241ec4fa836bf8d2977c0b8e8172dd69cf",
"bin": "knope-x86_64-unknown-linux-musl/knope"
},
"x86_64_macos": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-x86_64-apple-darwin.tgz",
"etag": "0x8DE8761D8E4D27D",
"hash": "010dc197bf159bbd9d60e897252248ba2b0e204beae7250ce54a9deae1ec4876",
"bin": "knope-x86_64-apple-darwin/knope"
},
"x86_64_windows": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-x86_64-pc-windows-msvc.tgz",
"etag": "0x8DE8761D8EAE61C",
"hash": "09f735b2da42cd594189042d1379c0a3a350a8c0ccb741015a84c6ff334543b1",
"bin": "knope-x86_64-pc-windows-msvc/knope.exe"
},
"aarch64_linux_musl": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-aarch64-unknown-linux-musl.tgz",
"etag": "0x8DE8761D8EE649C",
"hash": "95e882afdb4154c5baaba91f7bbd1fb1d41cec6898363a2b30e7abad4057b83b",
"bin": "knope-aarch64-unknown-linux-musl/knope"
},
"aarch64_macos": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-aarch64-apple-darwin.tgz",
"etag": "0x8DE8761D8EC1D47",
"hash": "02131f284315c8ece8a4ef69a0aff5f658309d4df73b95cfdfbe0fbd9e9ce259",
"bin": "knope-aarch64-apple-darwin/knope"
}
},
"0.22.3": {
"x86_64_linux_musl": {

32
manifests/mise.json generated
View File

@@ -28,13 +28,39 @@
},
"license_markdown": "[MIT](https://github.com/jdx/mise/blob/main/LICENSE)",
"latest": {
"version": "2026.3.9"
"version": "2026.3.10"
},
"2026": {
"version": "2026.3.9"
"version": "2026.3.10"
},
"2026.3": {
"version": "2026.3.9"
"version": "2026.3.10"
},
"2026.3.10": {
"x86_64_linux_musl": {
"etag": "0x8DE8749F9B8C65D",
"hash": "b0c2fd25fe95cd1ed3f178b95690608472aa8a27ce2f6e63eaebda52a238e570"
},
"x86_64_macos": {
"etag": "0x8DE8749FBD637A3",
"hash": "5ed1a2a6a79aab33e67d21156ec42b22d3cde1ceef09eb08c0ccd9b429795e6a"
},
"x86_64_windows": {
"etag": "0x8DE8749FC98ACBC",
"hash": "bf5e86077f652caca0413155e33886c3459d3f2963f9f186be76c8c05c2accb6"
},
"aarch64_linux_musl": {
"etag": "0x8DE8749F6470833",
"hash": "9730abf52c93c7945f907f4fe6f731b79d74671705656fa36fa45008933e88c7"
},
"aarch64_macos": {
"etag": "0x8DE8749FB48C7AB",
"hash": "85b5e577a5ed34431718091122ea7ec9cf7d4e1d8e5e4dc298cdb02d8dbd97b3"
},
"aarch64_windows": {
"etag": "0x8DE8749FC8C37A5",
"hash": "82a702481c9e877b28f82eb60a0d3be2d393fc9b7915283992b1cd0263724d2b"
}
},
"2026.3.9": {
"x86_64_linux_musl": {

30
manifests/tombi.json generated
View File

@@ -22,10 +22,36 @@
},
"license_markdown": "[MIT](https://github.com/tombi-toml/tombi/blob/main/LICENSE)",
"latest": {
"version": "0.9.8"
"version": "0.9.9"
},
"0.9": {
"version": "0.9.8"
"version": "0.9.9"
},
"0.9.9": {
"x86_64_linux_musl": {
"etag": "0x8DE86ED14380D61",
"hash": "4317ffc3e08fc6e3ba92953af85b6ad361493fd79916949aec5b5b7af471922f"
},
"x86_64_macos": {
"etag": "0x8DE86ED1495C95E",
"hash": "7f43d90e17f0a6406b5c53bb2773ea1f51e9aba00d47e76d6a55f70ba6e519e0"
},
"x86_64_windows": {
"etag": "0x8DE86ED1433F34E",
"hash": "efb2e3bd949435d244368179476658b5bed9bf35b662face2c00920faf97f247"
},
"aarch64_linux_musl": {
"etag": "0x8DE86ED1432BC2D",
"hash": "75d32d6d17a35c5307f7b67c4f80eca8d43b4d5bdacf86505a4bb87dcb3100d0"
},
"aarch64_macos": {
"etag": "0x8DE86ED149A3137",
"hash": "4e49e41a2f3bd7a26e11c0ce936ac0b75d60c9068798b951ea925cea04ce4a5b"
},
"aarch64_windows": {
"etag": "0x8DE86ED14385B2B",
"hash": "138047f5a495629a1fe60734706ab1d89fc8ed68eeab3dbd0b1865eab108f7b6"
}
},
"0.9.8": {
"x86_64_linux_musl": {

View File

@@ -4,6 +4,9 @@
"rust_crate": "${package}",
"bin": "mise/bin/${package}${exe}",
"version_range": ">= 2025.9.7",
"signing": {
"kind": "custom"
},
"platform": {
"x86_64_linux_musl": {
"asset_name": "${package}-v${version}-${rust_target_os}-x64-musl.tar.gz"

View File

@@ -3,6 +3,10 @@
"tag_prefix": "v",
"bin": "${package}${exe}",
"version_range": ">= 0.83.0",
"signing": {
"version_range": ">= 0.104.0",
"kind": "custom"
},
"platform": {
"x86_64_linux_musl": {
"asset_name": "${package}_${version}_linux_amd64.tar.gz"

View File

@@ -61,6 +61,18 @@ impl BaseManifest {
if self.platform.is_empty() {
panic!("At least one platform must be specified");
}
if let Some(website) = &self.website {
if website.is_empty() || *website == self.repository {
panic!(
"Please do not put the repository in website, or set website to an empty value"
);
}
}
if let Some(license_markdown) = &self.license_markdown {
if license_markdown.is_empty() {
panic!("license_markdown can not be an empty value");
}
}
}
}
@@ -83,6 +95,8 @@ pub enum SigningKind {
/// public key: package.metadata.binstall.signing.pubkey at Cargo.toml
/// <https://github.com/cargo-bins/cargo-binstall/blob/HEAD/SIGNING.md>
MinisignBinstall,
/// tool-specific
Custom,
}
#[derive(Debug, Deserialize)]

View File

@@ -9,7 +9,7 @@ use std::{
env,
ffi::OsStr,
io::Read as _,
path::Path,
path::{Path, PathBuf},
sync::{LazyLock, RwLock},
time::Duration,
};
@@ -20,9 +20,10 @@ use install_action_internal_codegen::{
BaseManifest, HostPlatform, Manifest, ManifestDownloadInfo, ManifestRef, ManifestTemplate,
ManifestTemplateDownloadInfo, Manifests, SigningKind, Version, workspace_root,
};
use serde::de::DeserializeOwned;
use spdx::expression::{ExprNode, ExpressionReq, Operator};
fn main() -> Result<()> {
fn main() {
let args: Vec<_> = env::args().skip(1).collect();
if args.is_empty() || args.iter().any(|arg| arg.starts_with('-')) {
println!(
@@ -30,7 +31,7 @@ fn main() -> Result<()> {
);
std::process::exit(1);
}
let package = &args[0];
let package = &*args[0];
let version_req = args.get(1);
let version_req_given = version_req.is_some();
let skip_existing_manifest_versions = std::env::var("SKIP_EXISTING_MANIFEST_VERSIONS").is_ok();
@@ -38,23 +39,25 @@ fn main() -> Result<()> {
let workspace_root = workspace_root();
let manifest_path = &workspace_root.join("manifests").join(format!("{package}.json"));
let download_cache_dir = &workspace_root.join("tools/codegen/tmp/cache").join(package);
fs::create_dir_all(manifest_path.parent().unwrap())?;
fs::create_dir_all(download_cache_dir)?;
fs::create_dir_all(manifest_path.parent().unwrap()).unwrap();
fs::create_dir_all(download_cache_dir).unwrap();
eprintln!("download cache: {}", download_cache_dir.display());
let mut base_info: BaseManifest = serde_json::from_slice(&fs::read(
workspace_root.join("tools/codegen/base").join(format!("{package}.json")),
)?)?;
let mut base_info: BaseManifest = serde_json::from_slice(
&fs::read(workspace_root.join("tools/codegen/base").join(format!("{package}.json")))
.unwrap(),
)
.unwrap();
base_info.validate();
let repo = base_info
.repository
.strip_prefix("https://github.com/")
.context("repository must start with https://github.com/")?;
.context("repository must start with https://github.com/")
.unwrap();
eprintln!("downloading metadata from {GITHUB_API_START}repos/{repo}");
let repo_info: github::RepoMetadata =
download(&format!("{GITHUB_API_START}repos/{repo}"))?.into_json()?;
let repo_info: github::RepoMetadata = download_json(&format!("{GITHUB_API_START}repos/{repo}"));
eprintln!("downloading releases from {GITHUB_API_START}repos/{repo}/releases");
let mut releases: github::Releases = vec![];
@@ -62,10 +65,9 @@ fn main() -> Result<()> {
// is greater than 100, multiple fetches are needed.
for page in 1.. {
let per_page = 100;
let mut r: github::Releases = download(&format!(
let mut r: github::Releases = download_json(&format!(
"{GITHUB_API_START}repos/{repo}/releases?per_page={per_page}&page={page}"
))?
.into_json()?;
));
// If version_req is latest, it is usually sufficient to look at the latest 100 releases.
if r.len() < per_page || version_req.is_some_and(|req| req == "latest") {
releases.append(&mut r);
@@ -103,23 +105,25 @@ fn main() -> Result<()> {
.rust_crate
.as_ref()
.map(|s| replace_vars(s, package, None, None, base_info.rust_crate.as_deref()))
.transpose()?;
.transpose()
.unwrap();
if let Some(crate_name) = &base_info.rust_crate {
eprintln!("downloading crate info from https://crates.io/api/v1/crates/{crate_name}");
let info = download(&format!("https://crates.io/api/v1/crates/{crate_name}"))?
.into_json::<crates_io::Crate>()?;
let info: crates_io::Crate =
download_json(&format!("https://crates.io/api/v1/crates/{crate_name}"));
let latest_version = &info.versions[0].num;
crates_io_version_detail = Some(
download(&format!("https://crates.io/api/v1/crates/{crate_name}/{latest_version}"))?
.into_json::<crates_io::VersionMetadata>()?
.version,
download_json::<crates_io::VersionMetadata>(&format!(
"https://crates.io/api/v1/crates/{crate_name}/{latest_version}"
))
.version,
);
if let Some(crate_repository) = info.crate_.repository.clone() {
if !crate_repository.to_lowercase().starts_with(&base_info.repository.to_lowercase()) {
panic!("repository {crate_repository} from crates.io differs from base manifest");
}
} else if crate_name != "zola" {
} else {
panic!("crate metadata does not include a repository");
}
@@ -139,7 +143,7 @@ fn main() -> Result<()> {
if manifest_path.is_file() {
println!("loading pre-existing manifest {}", manifest_path.display());
match serde_json::from_slice(&fs::read(manifest_path)?) {
match serde_json::from_slice(&fs::read(manifest_path).unwrap()) {
Ok(m) => {
manifests = m;
for (k, manifest) in &mut manifests.map {
@@ -165,18 +169,8 @@ fn main() -> Result<()> {
}
}
// Check website
if let Some(website) = base_info.website {
if website.is_empty() || website == base_info.repository {
panic!("Please do not put the repository in website, or set website to an empty value");
}
}
// Populate license_markdown from the base manifest if present.
if let Some(license_markdown) = base_info.license_markdown {
if license_markdown.is_empty() {
panic!("license_markdown can not be an empty value");
}
manifests.license_markdown = license_markdown;
}
@@ -184,7 +178,7 @@ fn main() -> Result<()> {
if !manifests.license_markdown.is_empty() {
let urls = get_license_markdown_urls(&manifests.license_markdown);
if urls.is_empty() {
bail!("Could not find URLs in license_markdown: {}.", manifests.license_markdown);
panic!("Could not find URLs in license_markdown: {}.", manifests.license_markdown);
}
for url in urls {
if let Err(err) = github_head(&url) {
@@ -207,7 +201,7 @@ fn main() -> Result<()> {
spdx_id
}
_ => {
bail!(
panic!(
"No license SPDX found in crates.io or GitHub metadata.\n\
Please set license_markdown in the base manifest"
);
@@ -218,7 +212,7 @@ fn main() -> Result<()> {
{
manifests.license_markdown = license_markdown;
} else {
bail!(
panic!(
"Unable to verify license file(s) in the repo for license {license}.\n\
Please set license_markdown in the base manifest"
);
@@ -227,13 +221,13 @@ fn main() -> Result<()> {
let version_req: semver::VersionReq = match version_req {
_ if latest_only => {
let req = format!("={}", releases.first_key_value().unwrap().0.0).parse()?;
let req = format!("={}", releases.first_key_value().unwrap().0.0).parse().unwrap();
eprintln!("update manifest for versions '{req}'");
req
}
None => match base_info.version_range {
Some(version_range) => version_range.parse()?,
None => ">= 0.0.1".parse()?, // HACK: ignore pre-releases
Some(version_range) => version_range.parse().unwrap(),
None => ">= 0.0.1".parse().unwrap(), // HACK: ignore pre-releases
},
Some(version_req) => {
for version in manifests.map.keys() {
@@ -249,18 +243,26 @@ fn main() -> Result<()> {
let req = if version_req == "latest" {
// TODO: this should check all missing versions
if manifests.map.is_empty() {
format!("={}", releases.first_key_value().unwrap().0.0).parse()?
format!("={}", releases.first_key_value().unwrap().0.0).parse().unwrap()
} else {
format!(">={}", semver_versions.last().unwrap()).parse()?
format!(">={}", semver_versions.last().unwrap()).parse().unwrap()
}
} else {
version_req.parse()?
version_req.parse().unwrap()
};
eprintln!("update manifest for versions '{req}'");
req
}
};
let signing_version_req: Option<semver::VersionReq> =
base_info.signing.as_ref().map(|signing| {
match &signing.version_range {
Some(version_range) => version_range.parse().unwrap(),
None => ">= 0.0.1".parse().unwrap(), // HACK: ignore pre-releases
}
});
let mut buf = vec![];
let mut buf2 = vec![];
for (Reverse(semver_version), (version, release)) in &releases {
@@ -282,11 +284,154 @@ fn main() -> Result<()> {
continue;
}
let signing_version_req: Option<semver::VersionReq> = match &base_info.signing {
let mut verified_checksum: Option<Vec<_>> = None;
match &base_info.signing {
Some(signing) => {
match &signing.version_range {
Some(version_range) => Some(version_range.parse()?),
None => Some(">= 0.0.1".parse()?), // HACK: ignore pre-releases
if let SigningKind::Custom = signing.kind {
match package {
_ if !signing_version_req.as_ref().unwrap().matches(semver_version) => {}
"mise" => {
// Refs: https://github.com/jdx/mise/blob/v2026.3.9/src/minisign.rs
let crates_io_info = crates_io_info.as_ref().unwrap();
let [checksum, sig] =
["SHASUMS256.txt", "SHASUMS256.txt.minisig"].map(|f| {
let Some(asset) =
release.assets.iter().find(|asset| asset.name == f)
else {
// There is broken release which has no release assets: https://github.com/jdx/mise/releases/tag/v2026.2.14
return PathBuf::new();
};
let download_cache =
download_cache_dir.join(format!("{version}-{f}"));
let url = &asset.browser_download_url;
eprint!("downloading {url} for signature verification ... ");
if download_cache.is_file() {
eprintln!("already downloaded");
} else {
download_to_buf(url, &mut buf);
eprintln!("download complete");
fs::write(&download_cache, &buf).unwrap();
buf.clear();
}
download_cache
});
if checksum.as_os_str().is_empty() || sig.as_os_str().is_empty() {
continue;
}
let v = crates_io_info
.versions
.iter()
.find(|v| v.num == *semver_version)
.unwrap();
let url = format!("https://crates.io{}", v.dl_path);
let pubkey_download_cache =
&download_cache_dir.join(format!("{version}-minisign.pub"));
eprint!("downloading {url} for signature verification ... ");
if pubkey_download_cache.is_file() {
eprintln!("already downloaded");
} else {
download_to_buf(&url, &mut buf);
let hash = ring::digest::digest(&ring::digest::SHA256, &buf);
if format!("{hash:?}").strip_prefix("SHA256:").unwrap()
!= v.checksum
{
panic!("checksum mismatch for {url}");
}
let decoder = flate2::read::GzDecoder::new(&*buf);
let mut archive = tar::Archive::new(decoder);
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap();
if path.file_name() == Some(OsStr::new("minisign.pub")) {
entry.unpack(pubkey_download_cache).unwrap();
break;
}
}
buf.clear();
eprintln!("download complete");
}
let pubkey =
minisign_verify::PublicKey::from_file(pubkey_download_cache)
.unwrap();
eprint!("verifying checksum file for {package}@{version} ... ");
let allow_legacy = false;
pubkey
.verify(
&fs::read(&checksum).unwrap(),
&minisign_verify::Signature::from_file(sig).unwrap(),
allow_legacy,
)
.unwrap();
verified_checksum = Some(
fs::read_to_string(checksum)
.unwrap()
.lines()
.filter_map(|l| l.split_once(" "))
.map(|(h, f)| {
(f.trim_ascii().to_owned(), h.trim_ascii().to_owned())
})
.collect(),
);
eprintln!("done");
}
"syft" => {
// Refs: https://oss.anchore.com/docs/installation/verification/
let [checksum, certificate, signature] =
["checksums.txt", "checksums.txt.pem", "checksums.txt.sig"].map(
|f| {
let asset = release
.assets
.iter()
.find(|asset| asset.name.ends_with(f))
.unwrap();
let download_cache =
download_cache_dir.join(format!("{version}-{f}"));
let url = &asset.browser_download_url;
eprint!(
"downloading {url} for signature verification ... "
);
if download_cache.is_file() {
eprintln!("already downloaded");
} else {
download_to_buf(url, &mut buf);
eprintln!("download complete");
fs::write(&download_cache, &buf).unwrap();
buf.clear();
}
download_cache
},
);
eprint!("verifying checksum file for {package}@{version} ... ");
cmd!(
"cosign",
"verify-blob",
&checksum,
"--certificate",
certificate,
"--signature",
signature,
"--certificate-identity-regexp",
format!("https://github\\.com/{repo}/\\.github/workflows/.+"),
"--certificate-oidc-issuer",
"https://token.actions.githubusercontent.com"
)
.run()
.unwrap();
verified_checksum = Some(
fs::read_to_string(checksum)
.unwrap()
.lines()
.filter_map(|l| l.split_once(" "))
.map(|(h, f)| {
(f.trim_ascii().to_owned(), h.trim_ascii().to_owned())
})
.collect(),
);
eprintln!("done");
}
_ => {}
}
}
}
None => {
@@ -294,24 +439,29 @@ fn main() -> Result<()> {
asset.name.contains(".asc")
|| asset.name.contains(".gpg")
|| asset.name.contains(".sig")
|| asset.name.contains(".minisig")
|| asset.name.contains(".pem")
|| asset.name.contains(".crt")
|| asset.name.contains(".key")
|| asset.name.contains(".pub")
}) {
eprintln!(
"{package} may supports other signing verification method using {}",
"{package} may supports other signature verification method using {}",
asset.name
);
}
None
}
};
}
let mut download_info = BTreeMap::new();
let mut pubkey = None;
let mut minisign_binstall_pubkey = None;
for (&platform, base_download_info) in &base_info.platform {
let asset_names = base_download_info
.asset_name
.as_ref()
.or(base_info.asset_name.as_ref())
.with_context(|| format!("asset_name is needed for {package} on {platform:?}"))?
.with_context(|| format!("asset_name is needed for {package} on {platform:?}"))
.unwrap()
.as_slice()
.iter()
.map(|asset_name| {
@@ -323,7 +473,8 @@ fn main() -> Result<()> {
base_info.rust_crate.as_deref(),
)
})
.collect::<Result<Vec<_>>>()?;
.collect::<Result<Vec<_>>>()
.unwrap();
let (url, digest, asset_name) = match asset_names.iter().find_map(|asset_name| {
release
.assets
@@ -345,7 +496,7 @@ fn main() -> Result<()> {
"{version}-{platform:?}-{}",
Path::new(&url).file_name().unwrap().to_str().unwrap()
));
let response = download(&url)?;
let response = download(&url).unwrap();
let etag =
response.header("etag").expect("binary should have an etag").replace('\"', "");
@@ -365,11 +516,11 @@ fn main() -> Result<()> {
if download_cache.is_file() {
eprintln!("already downloaded");
fs::File::open(download_cache)?.read_to_end(&mut buf)?; // Not buffered because it is read at once.
fs::File::open(download_cache).unwrap().read_to_end(&mut buf).unwrap(); // Not buffered because it is read at once.
} else {
response.into_reader().read_to_end(&mut buf)?;
response.into_reader().read_to_end(&mut buf).unwrap();
eprintln!("download complete");
fs::write(download_cache, &buf)?;
fs::write(download_cache, &buf).unwrap();
}
eprintln!("getting sha256 hash for {url}");
@@ -377,7 +528,7 @@ fn main() -> Result<()> {
let hash = format!("{hash:?}").strip_prefix("SHA256:").unwrap().to_owned();
if let Some(digest) = digest {
if hash != digest.strip_prefix("sha256:").unwrap() {
bail!(
panic!(
"digest mismatch between GitHub release page and actually downloaded file"
);
}
@@ -401,9 +552,15 @@ fn main() -> Result<()> {
signer_workflow,
&download_cache
)
.run()?;
.run()
.unwrap();
}
SigningKind::MinisignBinstall => {
let Some(crates_io_info) = &crates_io_info else {
panic!(
"signing kind minisign-binstall is supported only for rust crate"
);
};
let url = url.clone() + ".sig";
let sig_download_cache = &download_cache.with_extension(format!(
"{}.sig",
@@ -412,19 +569,14 @@ fn main() -> Result<()> {
eprint!("downloading {url} for signature validation ... ");
let sig = if sig_download_cache.is_file() {
eprintln!("already downloaded");
minisign_verify::Signature::from_file(sig_download_cache)?
minisign_verify::Signature::from_file(sig_download_cache).unwrap()
} else {
let buf = download(&url)?.into_string()?;
let buf = download(&url).unwrap().into_string().unwrap();
eprintln!("download complete");
fs::write(sig_download_cache, &buf)?;
minisign_verify::Signature::decode(&buf)?
fs::write(sig_download_cache, &buf).unwrap();
minisign_verify::Signature::decode(&buf).unwrap()
};
let Some(crates_io_info) = &crates_io_info else {
bail!(
"signing kind minisign-binstall is supported only for rust crate"
);
};
let v = crates_io_info
.versions
.iter()
@@ -437,28 +589,29 @@ fn main() -> Result<()> {
if crate_download_cache.is_file() {
eprintln!("already downloaded");
} else {
download(&url)?.into_reader().read_to_end(&mut buf2)?;
download_to_buf(&url, &mut buf2);
let hash = ring::digest::digest(&ring::digest::SHA256, &buf2);
if format!("{hash:?}").strip_prefix("SHA256:").unwrap() != v.checksum {
bail!("checksum mismatch for {url}");
panic!("checksum mismatch for {url}");
}
let decoder = flate2::read::GzDecoder::new(&*buf2);
let mut archive = tar::Archive::new(decoder);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap();
if path.file_name() == Some(OsStr::new("Cargo.toml")) {
entry.unpack(crate_download_cache)?;
entry.unpack(crate_download_cache).unwrap();
break;
}
}
buf2.clear();
eprintln!("download complete");
}
if pubkey.is_none() {
if minisign_binstall_pubkey.is_none() {
let cargo_manifest = toml::de::from_str::<cargo_manifest::Manifest>(
&fs::read_to_string(crate_download_cache)?,
)?;
&fs::read_to_string(crate_download_cache).unwrap(),
)
.unwrap();
eprintln!(
"algorithm: {}",
cargo_manifest.package.metadata.binstall.signing.algorithm
@@ -471,16 +624,42 @@ fn main() -> Result<()> {
cargo_manifest.package.metadata.binstall.signing.algorithm,
"minisign"
);
pubkey = Some(minisign_verify::PublicKey::from_base64(
&cargo_manifest.package.metadata.binstall.signing.pubkey,
)?);
minisign_binstall_pubkey = Some(
minisign_verify::PublicKey::from_base64(
&cargo_manifest.package.metadata.binstall.signing.pubkey,
)
.unwrap(),
);
}
let pubkey = pubkey.as_ref().unwrap();
let pubkey = minisign_binstall_pubkey.as_ref().unwrap();
eprint!("verifying signature for {bin_url} ... ");
let allow_legacy = false;
pubkey.verify(&buf, &sig, allow_legacy)?;
pubkey.verify(&buf, &sig, allow_legacy).unwrap();
eprintln!("done");
}
SigningKind::Custom => {
if let Some(verified_checksum) = &verified_checksum {
let asset_name_cwd = format!("./{asset_name}");
let mut checked = false;
for (f, h) in verified_checksum {
if *f == asset_name || *f == asset_name_cwd {
checked = true;
assert_eq!(
hash, *h,
"verified checksum doesn't match with sha256 hash of {asset_name} in {package}@{version}"
);
}
}
assert!(
checked,
"{asset_name} not found in verified checksum for {package}@{version}"
);
} else {
unimplemented!(
"unimplemented tool-specific signing handling for {package}"
);
}
}
}
}
@@ -563,7 +742,7 @@ fn main() -> Result<()> {
// update an existing manifests.json to avoid discarding work done in the event of a fetch error.
if existing_manifest.is_some() && !version_req_given {
write_manifests(manifest_path, &manifests.clone())?;
write_manifests(manifest_path, &manifests.clone()).unwrap();
eprintln!("wrote {} with incomplete data", manifest_path.display());
}
}
@@ -636,7 +815,7 @@ fn main() -> Result<()> {
.values()
.any(|m| matches!(m, ManifestRef::Real(m) if m.download_info.contains_key(&p)))
{
bail!(
panic!(
"platform list in base manifest for {package} contains {p:?}, \
but result manifest doesn't contain it; \
consider removing {p:?} from platform list in base manifest"
@@ -680,7 +859,7 @@ fn main() -> Result<()> {
// until 2027-08, people aren't paying much attention to it at this time.
continue;
}
bail!(
panic!(
"platform list in base manifest for {package} contains {p:?}, \
but latest release ({latest_version}) doesn't contain it; \
consider marking {latest_version} as broken by adding 'broken' field to base manifest"
@@ -720,10 +899,8 @@ fn main() -> Result<()> {
manifests.rust_crate = base_info.rust_crate;
write_manifests(manifest_path, &manifests)?;
write_manifests(manifest_path, &manifests).unwrap();
eprintln!("wrote {}", manifest_path.display());
Ok(())
}
fn write_manifests(manifest_path: &Path, manifests: &Manifests) -> Result<()> {
@@ -858,6 +1035,16 @@ fn download(url: &str) -> Result<ureq::Response> {
Err(last_error.unwrap().into())
}
#[track_caller]
fn download_to_buf(url: &str, buf: &mut Vec<u8>) {
download(url).unwrap().into_reader().read_to_end(buf).unwrap();
}
#[track_caller]
fn download_json<T: DeserializeOwned>(url: &str) -> T {
download(url).unwrap().into_json().unwrap()
}
fn github_head(url: &str) -> Result<()> {
eprintln!("fetching head of {url} ..");
let mut token = GITHUB_TOKENS.get(url);

View File

@@ -6,7 +6,6 @@ use std::{
path::PathBuf,
};
use anyhow::Result;
use fs_err as fs;
use install_action_internal_codegen::{BaseManifest, Manifests, workspace_root};
@@ -32,7 +31,7 @@ const FOOTER: &str = "
[cargo-binstall]: https://github.com/cargo-bins/cargo-binstall
";
fn main() -> Result<()> {
fn main() {
let args: Vec<_> = env::args().skip(1).collect();
if !args.is_empty() || args.iter().any(|arg| arg.starts_with('-')) {
println!(
@@ -72,9 +71,10 @@ fn main() -> Result<()> {
name.set_extension("");
let name = name.to_string_lossy().to_string();
let base_info: BaseManifest =
serde_json::from_slice(&fs::read(base_info_dir.join(file_name.clone()))?)?;
serde_json::from_slice(&fs::read(base_info_dir.join(file_name.clone())).unwrap())
.unwrap();
let manifests: Manifests =
serde_json::from_slice(&fs::read(manifest_dir.join(file_name))?)?;
serde_json::from_slice(&fs::read(manifest_dir.join(file_name)).unwrap()).unwrap();
let website = match base_info.website {
Some(website) => website,
@@ -135,9 +135,7 @@ fn main() -> Result<()> {
}
file.write_all(FOOTER.as_bytes()).expect("Unable to write footer");
file.flush()?;
Ok(())
file.flush().unwrap();
}
#[derive(Debug)]

View File

@@ -22,6 +22,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
- Remove `BaseManifest` and related types since they are unrelated to public manifests.
- Enable [release immutability](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases).
## [0.1.1] - 2025-09-20
- Add `HostPlatform::{powerpc64le_linux_gnu,powerpc64le_linux_musl,riscv64_linux_gnu,riscv64_linux_musl,s390x_linux_gnu,s390x_linux_musl}` ([#1133](https://github.com/taiki-e/install-action/pull/1133))

View File

@@ -11,6 +11,11 @@ cd -- "$(dirname -- "$0")"/..
# ./tools/manifest.sh [PACKAGE [VERSION_REQ]]
# ./tools/manifest.sh full
if [[ -n "${GITHUB_ACTIONS:-}" ]] && ! type -P cosign; then
go install github.com/sigstore/cosign/v3/cmd/cosign@latest
sudo mv -- ~/go/bin/cosign /usr/local/bin
fi
if [[ $# -eq 1 ]] && [[ "$1" == "full" ]]; then
for manifest in tools/codegen/base/*.json; do
package="${manifest##*/}"