Skip to main content

ant_core/
update.rs

1use std::path::{Path, PathBuf};
2
3use ant_protocol::pqc::api::{ml_dsa_65, MlDsaPublicKey, MlDsaSignature, MlDsaVariant};
4use futures_util::StreamExt;
5use serde::{Deserialize, Serialize};
6
7use crate::channel::version_matches_channel;
8use crate::error::{Error, Result};
9use crate::node::binary::{extract_tar_gz, extract_zip, ProgressReporter};
10use crate::node::types::UpgradeChannel;
11
12const GITHUB_REPO: &str = "WithAutonomi/ant-client";
13const CLI_BINARY_NAME: &str = "ant";
14const TAG_PREFIX: &str = "ant-cli-v";
15
16/// Signing context for domain separation (must match the context used by `ant-keygen sign`).
17const SIGNING_CONTEXT: &[u8] = b"ant-release-v1";
18
19/// ML-DSA-65 signature size in bytes.
20const SIGNATURE_SIZE: usize = 3309;
21
22/// Embedded release signing public key (ML-DSA-65).
23///
24/// This key is used to verify signatures on released ant-client binaries.
25/// The corresponding private key is held by authorised release signers.
26/// Generated: 2026-03-30 19:49:36 UTC
27const RELEASE_SIGNING_KEY: &[u8] = &[
28    0xb4, 0xcf, 0x2a, 0x24, 0x31, 0xd9, 0xb2, 0x3a, 0xab, 0xe9, 0x5e, 0xfc, 0xbc, 0xf3, 0xb1, 0x1f,
29    0x4e, 0x50, 0x0a, 0x46, 0xeb, 0x83, 0xfc, 0x6f, 0x0f, 0x89, 0x41, 0x00, 0x1b, 0x52, 0xde, 0xdc,
30    0xb5, 0xc9, 0x07, 0xed, 0x72, 0x3a, 0xe1, 0xa9, 0x82, 0xe1, 0xfc, 0xff, 0xab, 0xce, 0x9f, 0x7d,
31    0xab, 0xe2, 0x57, 0x92, 0xe0, 0xf2, 0xac, 0xa1, 0x41, 0xd3, 0x74, 0x95, 0x41, 0xd1, 0xac, 0x22,
32    0xb1, 0xbb, 0x5c, 0xf4, 0x02, 0x0b, 0x73, 0x85, 0xfd, 0x56, 0x75, 0x0d, 0x5c, 0x38, 0xe1, 0x2d,
33    0xe0, 0x15, 0x4f, 0xbf, 0x40, 0xeb, 0xf1, 0x0d, 0x8f, 0x39, 0x32, 0xeb, 0x80, 0xa7, 0x2e, 0x77,
34    0x3e, 0x54, 0xe3, 0x3d, 0x62, 0xae, 0xe7, 0x09, 0x5e, 0xfb, 0xdc, 0xaa, 0x07, 0xdc, 0xe1, 0x08,
35    0x96, 0xbb, 0x4b, 0xa0, 0x2e, 0x05, 0x2d, 0xee, 0xeb, 0x9a, 0x1a, 0xae, 0xde, 0xe9, 0x2c, 0xf8,
36    0x2f, 0x43, 0x6c, 0x78, 0x4b, 0xde, 0xef, 0x91, 0x8b, 0x94, 0x9d, 0x4f, 0x81, 0x05, 0xcc, 0xf0,
37    0x98, 0xce, 0xce, 0x67, 0x54, 0xac, 0xde, 0xcd, 0x26, 0x9e, 0x84, 0xf4, 0x88, 0xb2, 0x1a, 0x3e,
38    0x93, 0x2e, 0xff, 0xa8, 0x45, 0x95, 0xd1, 0xd0, 0xb1, 0x6c, 0x3c, 0x1e, 0xef, 0x3d, 0xe3, 0xf2,
39    0x73, 0xe2, 0xf6, 0xb7, 0xf9, 0x3f, 0x10, 0x0d, 0x3c, 0xde, 0x28, 0x94, 0x07, 0xef, 0x24, 0x70,
40    0xc4, 0x5a, 0x0a, 0x67, 0xbb, 0x0f, 0x4f, 0x5c, 0x2b, 0xd8, 0x02, 0x05, 0xa5, 0x98, 0x03, 0x5d,
41    0x8f, 0xc0, 0x4a, 0x84, 0xe9, 0xea, 0xac, 0x13, 0xdf, 0x69, 0xfc, 0x1e, 0xcf, 0xb6, 0x88, 0xba,
42    0x99, 0x30, 0xbc, 0x7a, 0xb8, 0x9d, 0x3d, 0x62, 0x3b, 0x33, 0x19, 0xbb, 0x3a, 0x2c, 0x2b, 0xa0,
43    0x5a, 0xb0, 0x8f, 0x9e, 0x10, 0x81, 0xb9, 0x12, 0x54, 0x81, 0xf8, 0xe2, 0x91, 0xb2, 0xe7, 0xe6,
44    0x9c, 0x11, 0xeb, 0x49, 0x64, 0x3a, 0x25, 0xd6, 0x53, 0x2e, 0xdf, 0xfc, 0x14, 0x32, 0x65, 0xcc,
45    0x87, 0xdb, 0xfd, 0xbb, 0x81, 0xa8, 0x50, 0xcc, 0xb4, 0x31, 0x0c, 0x70, 0xf3, 0xb6, 0x15, 0x8a,
46    0x50, 0x80, 0xad, 0xb1, 0xb0, 0x10, 0x2b, 0x67, 0x33, 0xf5, 0xf6, 0x36, 0x35, 0x5f, 0xa7, 0xd2,
47    0x81, 0xd6, 0x75, 0xa1, 0x18, 0x15, 0xbe, 0x1d, 0x5e, 0x33, 0x8e, 0x98, 0xdd, 0x45, 0x0f, 0x0c,
48    0x0f, 0x0d, 0x8b, 0x3f, 0x97, 0x11, 0x21, 0x2e, 0xa0, 0x5e, 0xfe, 0x70, 0x09, 0xa7, 0x14, 0x30,
49    0xa3, 0x01, 0x2d, 0x18, 0x2b, 0x8f, 0x19, 0x75, 0x54, 0x1f, 0xd8, 0xee, 0x66, 0x06, 0x7b, 0x9d,
50    0x7d, 0xb2, 0xae, 0x14, 0xe6, 0x51, 0x19, 0xc2, 0x45, 0x2e, 0x7e, 0x11, 0xd9, 0x7b, 0x16, 0x8e,
51    0xae, 0x17, 0xdb, 0x1b, 0x24, 0x90, 0xcd, 0xed, 0x94, 0xf9, 0xf7, 0xba, 0x9f, 0x4c, 0x12, 0xae,
52    0x31, 0x7b, 0xd4, 0x7c, 0x04, 0x42, 0x2c, 0x32, 0x16, 0xc1, 0x70, 0x6d, 0x11, 0x6f, 0x3b, 0x44,
53    0x62, 0xba, 0xbd, 0xc5, 0x7a, 0xec, 0x55, 0x1b, 0xcd, 0xdb, 0xb6, 0x55, 0x08, 0x86, 0x13, 0x7f,
54    0x4e, 0xa9, 0x63, 0xe1, 0x87, 0xa0, 0x7e, 0x49, 0xfb, 0xf4, 0xa3, 0x46, 0xcf, 0x1d, 0xec, 0xf5,
55    0xc6, 0x2f, 0xe1, 0x43, 0x02, 0xd0, 0xe4, 0x5f, 0x1b, 0x20, 0x1a, 0xa7, 0x81, 0xbd, 0x31, 0x19,
56    0x6a, 0x74, 0xd7, 0x9a, 0x6d, 0x3d, 0xf8, 0xac, 0x4d, 0xbb, 0x01, 0x63, 0xa4, 0x9d, 0x3c, 0xc9,
57    0x6c, 0x8a, 0x4f, 0x61, 0xd6, 0x98, 0xf5, 0x40, 0x22, 0xa9, 0x5e, 0x93, 0x5e, 0x13, 0xd3, 0xe0,
58    0xdb, 0x54, 0xab, 0x0d, 0xe3, 0x88, 0x85, 0x80, 0x7a, 0x5e, 0x38, 0x64, 0x97, 0xc4, 0xe9, 0xb0,
59    0x5d, 0xf7, 0x40, 0x5f, 0x6e, 0x3f, 0xbe, 0x14, 0x9a, 0x7c, 0xa2, 0x7c, 0x74, 0xa5, 0x32, 0x22,
60    0x61, 0x31, 0xa5, 0x0d, 0xa5, 0xcc, 0x93, 0xe4, 0xfd, 0xed, 0xbc, 0xe7, 0xf2, 0xe5, 0xdb, 0xc6,
61    0x0c, 0xe1, 0xc8, 0x4e, 0xee, 0xe6, 0x76, 0x1c, 0x10, 0x1b, 0xd8, 0x53, 0xd4, 0xe8, 0x07, 0xed,
62    0xea, 0x91, 0xd4, 0x1b, 0x91, 0x5c, 0x28, 0x05, 0xca, 0xe2, 0x9c, 0xd1, 0x99, 0x43, 0xed, 0xd8,
63    0x6a, 0x2b, 0xd2, 0x64, 0x9b, 0xe1, 0x0c, 0x88, 0x6c, 0x0d, 0xb2, 0x6b, 0x73, 0x85, 0x9d, 0xbf,
64    0x79, 0x78, 0xaa, 0x7b, 0x5e, 0xf8, 0xa4, 0x26, 0xdf, 0xb3, 0x9b, 0x24, 0x5a, 0xc8, 0x19, 0x22,
65    0xa5, 0xc6, 0xca, 0x00, 0x59, 0x3b, 0xad, 0x45, 0xdf, 0x71, 0x1d, 0x60, 0x60, 0x24, 0x0d, 0xa4,
66    0x3d, 0x42, 0x23, 0xb8, 0xfe, 0xac, 0x86, 0x94, 0x79, 0x87, 0x05, 0xae, 0xb8, 0x4d, 0x7e, 0x11,
67    0x5b, 0x22, 0x44, 0x15, 0x3d, 0x7f, 0x82, 0x98, 0x65, 0x0a, 0x3c, 0xd3, 0xef, 0x80, 0x0d, 0x75,
68    0x03, 0x92, 0xf6, 0x3a, 0x8b, 0xa4, 0xb0, 0x61, 0x2d, 0x2c, 0xcc, 0x1f, 0x01, 0x8e, 0x7a, 0x46,
69    0x36, 0x2a, 0x83, 0x21, 0x88, 0x98, 0x13, 0x0a, 0xd5, 0xa1, 0x54, 0x4b, 0x63, 0xe0, 0xe3, 0x1c,
70    0x07, 0x5e, 0x32, 0x8c, 0xa4, 0x6b, 0x62, 0xc3, 0x28, 0x95, 0xb8, 0x0a, 0xb9, 0x4f, 0xaf, 0x7f,
71    0x49, 0xeb, 0xff, 0xd7, 0xa1, 0x41, 0x43, 0x9a, 0x92, 0x9a, 0x5f, 0xee, 0xbd, 0xb9, 0xbe, 0xb3,
72    0x4b, 0x9d, 0x0b, 0xcb, 0x9b, 0x2c, 0x26, 0x8f, 0x0f, 0xb1, 0xfa, 0xc0, 0xe3, 0x3a, 0x7f, 0x2b,
73    0x51, 0x79, 0x75, 0x25, 0x5b, 0x23, 0x22, 0xb1, 0x01, 0x27, 0x4e, 0x43, 0xdd, 0x66, 0x7a, 0x33,
74    0x4d, 0x32, 0x96, 0x83, 0x59, 0x52, 0xd7, 0x3c, 0xb0, 0xe3, 0x03, 0xd6, 0xb0, 0xc7, 0x99, 0x68,
75    0xc6, 0xa4, 0x2d, 0x35, 0x3d, 0xa4, 0x6a, 0x17, 0xd9, 0xf4, 0x0c, 0x26, 0x11, 0xe4, 0xbc, 0x03,
76    0x87, 0x25, 0x62, 0xad, 0xa9, 0x7e, 0x96, 0x4d, 0x39, 0x9b, 0x8f, 0x09, 0xdc, 0xd1, 0x28, 0x5e,
77    0xf4, 0xe3, 0x94, 0xfd, 0x94, 0x46, 0x30, 0xe2, 0x24, 0x46, 0x30, 0x7f, 0xf4, 0x4c, 0xaa, 0x51,
78    0x7e, 0x04, 0x5c, 0xa4, 0x8c, 0xba, 0x4a, 0xb8, 0x61, 0x5e, 0x75, 0x1c, 0xa8, 0x0c, 0xbc, 0x7f,
79    0x36, 0x16, 0xa1, 0x72, 0x98, 0x6a, 0x44, 0x39, 0x42, 0x67, 0xb5, 0x4a, 0xac, 0x14, 0x35, 0x8f,
80    0xcd, 0x87, 0x3f, 0x9e, 0x2e, 0xa1, 0x53, 0xf1, 0x45, 0x68, 0x26, 0xcb, 0x35, 0x96, 0x57, 0xd5,
81    0x3a, 0x24, 0x74, 0xe2, 0xff, 0xe0, 0x70, 0xb1, 0xbd, 0xec, 0x0c, 0xd2, 0x97, 0x9a, 0xe5, 0x9f,
82    0xa9, 0xfe, 0x6a, 0x63, 0x17, 0x35, 0xad, 0x64, 0x2f, 0xd9, 0x2e, 0xdb, 0x47, 0xdc, 0x62, 0xdc,
83    0xcc, 0xee, 0x7e, 0x23, 0xa6, 0x67, 0x61, 0x7c, 0xd1, 0x03, 0xbd, 0x78, 0xe9, 0x34, 0x05, 0xed,
84    0x05, 0x87, 0xef, 0x59, 0xf4, 0x16, 0xd6, 0x8d, 0x85, 0x46, 0x65, 0x2a, 0x08, 0xac, 0x4a, 0x5d,
85    0xe6, 0x27, 0x5f, 0x43, 0xdd, 0x51, 0x4e, 0x95, 0x9b, 0xf5, 0x0c, 0x81, 0x24, 0x73, 0x39, 0x77,
86    0xe9, 0xc8, 0x35, 0x4a, 0xe2, 0xb8, 0x35, 0x92, 0xde, 0x5c, 0x31, 0x12, 0x36, 0x5c, 0xc7, 0x69,
87    0xcd, 0x79, 0xa9, 0xf9, 0xcf, 0x13, 0xa9, 0x12, 0x29, 0x25, 0x5c, 0x6a, 0x34, 0xa4, 0xbf, 0xc5,
88    0xb6, 0x2a, 0xc1, 0xba, 0x6a, 0xd3, 0x98, 0x8c, 0x9b, 0x6d, 0x9f, 0xb9, 0x25, 0xa6, 0xd1, 0x97,
89    0x80, 0x38, 0x11, 0xdc, 0x73, 0x5c, 0xe7, 0x3a, 0x1f, 0xd2, 0x16, 0xcd, 0x63, 0xfb, 0x41, 0xb0,
90    0xba, 0xb0, 0x38, 0x67, 0x48, 0xd2, 0x8a, 0x94, 0x2f, 0x11, 0x81, 0xbf, 0x66, 0x38, 0x68, 0xff,
91    0xfe, 0xd1, 0x7c, 0xcd, 0xa3, 0xac, 0xe4, 0xf7, 0x58, 0x19, 0xcd, 0x2a, 0xe3, 0xfa, 0x4d, 0xb0,
92    0xbe, 0xac, 0x05, 0x1c, 0xd9, 0x8d, 0xf7, 0x5c, 0xc0, 0xfc, 0xa6, 0xb5, 0x99, 0xb8, 0x8e, 0x2b,
93    0x72, 0xf8, 0x19, 0xfc, 0x17, 0x11, 0xf6, 0x2b, 0x08, 0xe4, 0x6e, 0xb0, 0x65, 0xab, 0x78, 0x8a,
94    0xfc, 0x7c, 0x09, 0xca, 0x73, 0xcd, 0x35, 0x5d, 0x6c, 0x7a, 0x36, 0xc0, 0x24, 0xba, 0x3f, 0x08,
95    0xea, 0x17, 0x09, 0xe1, 0x9d, 0x5d, 0x18, 0x59, 0x8a, 0xd8, 0x6a, 0x6d, 0x85, 0x6a, 0x9e, 0xa9,
96    0xe5, 0x4b, 0x45, 0xb2, 0x35, 0x6e, 0x62, 0x24, 0x08, 0x00, 0x1c, 0x06, 0x73, 0x27, 0x5d, 0x11,
97    0x4a, 0xc8, 0x51, 0xbd, 0x59, 0xd6, 0x94, 0xce, 0x16, 0x15, 0x17, 0x58, 0x7f, 0x39, 0x9d, 0x4e,
98    0x69, 0x1a, 0x64, 0xbb, 0xd4, 0x51, 0xb9, 0xe4, 0x7d, 0x51, 0x3a, 0xff, 0xe5, 0x1f, 0x29, 0xea,
99    0x7e, 0xa5, 0x62, 0x63, 0xff, 0x10, 0xf7, 0x54, 0x35, 0xd1, 0xf3, 0x73, 0x1e, 0xab, 0xca, 0x52,
100    0x14, 0xc6, 0x7e, 0x51, 0xc2, 0x48, 0x13, 0xcb, 0x30, 0xb2, 0x1a, 0x84, 0x72, 0xe5, 0x44, 0x83,
101    0xc9, 0x90, 0xa5, 0x8c, 0xf9, 0xeb, 0x3c, 0x5c, 0xc6, 0xcc, 0x8a, 0x95, 0x8a, 0xfa, 0xeb, 0x37,
102    0x9c, 0xde, 0xa2, 0xb1, 0x72, 0x4d, 0xd9, 0x3d, 0xab, 0xfd, 0x0e, 0xbd, 0x32, 0x9d, 0x23, 0xe9,
103    0x6f, 0x85, 0x4e, 0xfe, 0xcd, 0x91, 0xfb, 0x82, 0x94, 0xee, 0x8b, 0xdf, 0x6a, 0xd9, 0x01, 0xa1,
104    0xc6, 0x22, 0x18, 0x01, 0x8d, 0x10, 0xd5, 0x87, 0x42, 0xd0, 0xbd, 0x23, 0x75, 0x44, 0x53, 0x46,
105    0xa5, 0xae, 0x00, 0x4c, 0x0e, 0x88, 0x4a, 0xa8, 0x3d, 0x4a, 0x30, 0xe0, 0x1a, 0xa4, 0xe5, 0x40,
106    0xb8, 0xe0, 0x12, 0x9c, 0x44, 0x03, 0xfb, 0x2e, 0x4e, 0xf5, 0x29, 0xdb, 0x09, 0x84, 0x55, 0xc7,
107    0x6c, 0xc6, 0x1f, 0xf9, 0xee, 0x0b, 0xa4, 0x91, 0x7d, 0x79, 0x27, 0x59, 0x75, 0x97, 0xec, 0x6a,
108    0xa8, 0xf8, 0x55, 0xa8, 0x45, 0xd4, 0xd7, 0xa6, 0xc1, 0xc4, 0x27, 0x35, 0xe8, 0x4f, 0x39, 0x89,
109    0x7d, 0x41, 0xf3, 0xf6, 0xd0, 0xb6, 0xf9, 0x91, 0xeb, 0x94, 0xf1, 0xbb, 0x17, 0x46, 0x9c, 0xd5,
110    0x5a, 0x53, 0x04, 0x2d, 0x12, 0x7c, 0x17, 0x6a, 0x36, 0xb5, 0xea, 0xf3, 0x5b, 0x96, 0x1b, 0xee,
111    0xce, 0xc4, 0xc0, 0x11, 0x5a, 0xbc, 0x0c, 0x29, 0xd0, 0x42, 0x1d, 0x16, 0x63, 0xea, 0x1e, 0x04,
112    0x2f, 0xe3, 0x17, 0xed, 0x33, 0xac, 0x56, 0x80, 0x34, 0x41, 0x41, 0x1e, 0x77, 0x80, 0x06, 0x9f,
113    0xbc, 0x2e, 0x78, 0xa1, 0x04, 0x00, 0x06, 0x6f, 0x36, 0x2f, 0xb7, 0xa5, 0x95, 0x37, 0x82, 0x9d,
114    0xef, 0x41, 0x08, 0x85, 0x3d, 0x53, 0xa7, 0xfb, 0xfe, 0xba, 0x8c, 0xb9, 0xae, 0xc7, 0x89, 0x11,
115    0x69, 0x4f, 0x62, 0xe6, 0xb6, 0x08, 0x6b, 0x35, 0x1c, 0x96, 0xb3, 0x7b, 0x40, 0x2d, 0xee, 0x07,
116    0x40, 0x52, 0x4f, 0x68, 0x60, 0xf4, 0xb9, 0xc3, 0x54, 0x9f, 0x22, 0x50, 0x88, 0x48, 0x6a, 0x28,
117    0x93, 0x46, 0x00, 0xe2, 0x4a, 0x85, 0x41, 0x78, 0x0e, 0x87, 0xc5, 0xeb, 0xfc, 0xd3, 0x5f, 0x4d,
118    0x24, 0xe4, 0x9d, 0xeb, 0x1d, 0x00, 0x73, 0x85, 0x25, 0x47, 0x9e, 0x8c, 0x5b, 0x88, 0xf4, 0x3b,
119    0x33, 0xf0, 0x3d, 0x3a, 0xa1, 0x28, 0xd3, 0x06, 0xb4, 0x7a, 0x4e, 0x5d, 0x31, 0x1b, 0xca, 0xf4,
120    0x3f, 0x70, 0x30, 0x49, 0x44, 0x29, 0x24, 0x14, 0x5e, 0x35, 0xc2, 0x6c, 0x92, 0x7e, 0xf8, 0x97,
121    0x0c, 0x51, 0x9d, 0x67, 0xc0, 0x10, 0xa9, 0x35, 0x48, 0x59, 0x6a, 0x33, 0xef, 0x40, 0x4e, 0x53,
122    0x10, 0x14, 0x2a, 0x12, 0x38, 0xe6, 0xc4, 0x63, 0x9c, 0x84, 0x85, 0x06, 0xaf, 0x3d, 0x3a, 0x84,
123    0x06, 0x60, 0x88, 0x32, 0xda, 0x2c, 0xe5, 0xc6, 0x59, 0xf1, 0xe0, 0x10, 0xe2, 0x3c, 0xe6, 0xbf,
124    0x32, 0x7d, 0x32, 0x39, 0x6d, 0xe4, 0xd9, 0xca, 0xe7, 0xf5, 0xf4, 0xa6, 0x5f, 0xb2, 0x33, 0x05,
125    0xb5, 0xad, 0x5f, 0xcb, 0x0b, 0x14, 0xaf, 0xeb, 0xc0, 0xec, 0x87, 0x85, 0x9b, 0x13, 0xb5, 0x8a,
126    0x98, 0xa9, 0x92, 0x13, 0x1b, 0x74, 0xec, 0xfd, 0xe1, 0xc1, 0x22, 0x06, 0x5d, 0x4f, 0x06, 0xc7,
127    0xdd, 0xc6, 0xf0, 0xc4, 0x01, 0x04, 0xad, 0x7f, 0x71, 0xbc, 0x74, 0x4d, 0xfd, 0x18, 0xa3, 0x56,
128    0x2c, 0x45, 0x28, 0x2b, 0x2f, 0xbc, 0x9b, 0xb8, 0x4b, 0xe6, 0x51, 0x75, 0x28, 0x0c, 0x27, 0x0e,
129    0xf7, 0x92, 0x8c, 0xc9, 0xde, 0x33, 0x1a, 0x65, 0x28, 0xc7, 0x01, 0x32, 0xa2, 0x36, 0x88, 0xb6,
130    0x64, 0x10, 0x03, 0xd6, 0xb7, 0x9f, 0x9d, 0x73, 0xe1, 0xa9, 0xc7, 0xdf, 0xe1, 0x0b, 0x39, 0x31,
131    0x77, 0xbc, 0x91, 0xf1, 0x45, 0x9a, 0xc5, 0x97, 0x28, 0xc0, 0x61, 0xc5, 0x23, 0x54, 0xad, 0xe3,
132    0x23, 0x18, 0x69, 0xf7, 0x27, 0xd0, 0x5b, 0xf2, 0x44, 0x62, 0xdc, 0x97, 0xce, 0x4e, 0x40, 0x76,
133    0x00, 0xde, 0xc2, 0xf9, 0x3a, 0x42, 0xfd, 0xd4, 0xd7, 0xe1, 0x85, 0xd8, 0xc9, 0x38, 0x91, 0xc1,
134    0x79, 0x87, 0x58, 0xf1, 0x26, 0x1a, 0x29, 0x02, 0xe3, 0x54, 0xde, 0x58, 0x64, 0x9d, 0xe6, 0x8e,
135    0x33, 0x70, 0x53, 0x43, 0x47, 0x90, 0xee, 0x6e, 0x0f, 0x8c, 0xb3, 0x9e, 0x47, 0x45, 0xfc, 0xa8,
136    0xe3, 0x52, 0x62, 0x74, 0x6d, 0xa2, 0xaf, 0x28, 0x9d, 0xdf, 0x1e, 0x69, 0x1f, 0x56, 0xbc, 0x49,
137    0xc1, 0xe5, 0xd6, 0xc4, 0xb5, 0x5c, 0x4d, 0x39, 0x49, 0x4b, 0xb4, 0xec, 0x56, 0x54, 0x9a, 0x15,
138    0x94, 0x0a, 0xcb, 0xa9, 0x10, 0x46, 0x03, 0x5c, 0x23, 0x2f, 0x29, 0xed, 0x72, 0xa1, 0x57, 0xfa,
139    0x58, 0xef, 0x21, 0x7e, 0xf2, 0x8b, 0xa7, 0x04, 0x51, 0xb4, 0x03, 0x5d, 0xd8, 0x48, 0xc0, 0xe5,
140    0x83, 0xb6, 0x7a, 0x6b, 0xcd, 0xfb, 0xda, 0x47, 0xe8, 0xa1, 0xae, 0x57, 0x74, 0x49, 0xc0, 0xf9,
141    0x4a, 0x6b, 0x3c, 0xb5, 0xd8, 0x27, 0x3d, 0x1d, 0x96, 0x39, 0x09, 0x65, 0x95, 0xdb, 0x01, 0xa3,
142    0x8b, 0x78, 0x8b, 0x07, 0x6d, 0x1c, 0x8b, 0x4b, 0x1d, 0x9d, 0x4a, 0x4f, 0xcb, 0xb8, 0xf6, 0x22,
143    0x73, 0x8a, 0x7b, 0xc8, 0xf2, 0x0a, 0xef, 0x03, 0x1e, 0xb7, 0x4d, 0x8f, 0xc0, 0xdf, 0x87, 0x88,
144    0x05, 0xe1, 0x0a, 0x30, 0xea, 0xde, 0xf3, 0xc2, 0xb6, 0x00, 0x3c, 0xd6, 0xff, 0x3b, 0xb5, 0x01,
145    0xfb, 0xd8, 0xb2, 0x65, 0x26, 0x5d, 0xa0, 0x5a, 0x7c, 0xef, 0x1d, 0x85, 0xbe, 0x51, 0xc2, 0x57,
146    0x0b, 0x27, 0x37, 0x71, 0x99, 0xf5, 0x87, 0x83, 0x68, 0x0b, 0x88, 0xed, 0x66, 0x9e, 0x37, 0x59,
147    0x84, 0x23, 0x72, 0xc3, 0x80, 0xac, 0xfe, 0x45, 0x5f, 0xdf, 0x31, 0xc4, 0x84, 0x07, 0x5a, 0x17,
148    0x28, 0xcd, 0x64, 0xb4, 0xe2, 0xa3, 0x0e, 0x2c, 0x15, 0x60, 0x77, 0xdc, 0x08, 0x45, 0x36, 0x37,
149    0x68, 0x50, 0xba, 0x03, 0x85, 0xb7, 0xed, 0xd0, 0x7b, 0xb2, 0xa1, 0x62, 0xbc, 0x70, 0x00, 0x9e,
150];
151
152/// Result of checking whether an update is available.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct UpdateCheck {
155    pub current_version: String,
156    pub latest_version: String,
157    pub update_available: bool,
158    pub download_url: Option<String>,
159    /// Release channel the check was performed against.
160    ///
161    /// `#[serde(default)]` keeps `--json` output from pre-channel versions deserializing.
162    #[serde(default = "default_channel")]
163    pub channel: UpgradeChannel,
164}
165
166/// Channel assumed when deserializing an `UpdateCheck` written before channels existed.
167fn default_channel() -> UpgradeChannel {
168    UpgradeChannel::Stable
169}
170
171/// Infer the release channel a running binary belongs to from its own version.
172///
173/// A build whose version carries a `beta` pre-release identifier is a beta build and stays
174/// on the beta channel unless told otherwise; everything else is stable. This keeps a beta
175/// user on beta with no stored configuration — the alternative, defaulting to stable, would
176/// self-update a beta build straight back onto the stable train.
177#[must_use]
178pub fn channel_for_version(version: &str) -> UpgradeChannel {
179    match parse_version(version) {
180        Ok(v) if v.pre.as_str().split('.').next() == Some("beta") => UpgradeChannel::Beta,
181        _ => UpgradeChannel::Stable,
182    }
183}
184
185impl UpdateCheck {
186    /// Force the check to report an update even if already on the latest version.
187    ///
188    /// Populates `download_url` using the current `latest_version`.
189    pub fn force(&mut self) -> Result<()> {
190        self.update_available = true;
191        self.download_url = Some(build_download_url(&self.latest_version)?);
192        Ok(())
193    }
194}
195
196/// Result of a completed update.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct UpdateResult {
199    pub previous_version: String,
200    pub new_version: String,
201}
202
203/// Check whether a newer version is available on GitHub Releases.
204///
205/// Compares `current_version` against the highest release tag eligible for `channel`,
206/// using semantic versioning. See [`crate::channel::version_matches_channel`] for which
207/// tags each channel accepts.
208pub async fn check_for_update(
209    current_version: &str,
210    channel: UpgradeChannel,
211) -> Result<UpdateCheck> {
212    let latest = fetch_latest_cli_version(channel).await?;
213    let current = parse_version(current_version)?;
214    let latest_parsed = parse_version(&latest)?;
215    let update_available = latest_parsed > current;
216
217    let download_url = if update_available {
218        Some(build_download_url(&latest)?)
219    } else {
220        None
221    };
222
223    Ok(UpdateCheck {
224        current_version: current_version.to_string(),
225        latest_version: latest,
226        update_available,
227        download_url,
228        channel,
229    })
230}
231
232/// Download and install the update, replacing the current binary.
233///
234/// Downloads the release archive and its detached `.sig` file, verifies the
235/// ML-DSA-65 signature against the embedded release key, extracts the binary,
236/// and replaces the running executable.
237pub async fn perform_update(
238    check: &UpdateCheck,
239    progress: &dyn ProgressReporter,
240) -> Result<UpdateResult> {
241    let download_url = check.download_url.as_deref().ok_or_else(|| {
242        Error::UpdateFailed("no download URL — are you already on the latest version?".to_string())
243    })?;
244
245    let tmp_dir = tempfile::tempdir()
246        .map_err(|e| Error::UpdateFailed(format!("failed to create temp directory: {e}")))?;
247
248    let (archive_path, archive_bytes) =
249        download_archive(download_url, tmp_dir.path(), progress).await?;
250
251    let sig_url = format!("{download_url}.sig");
252    progress.report_started("Downloading signature...");
253    let sig_bytes = download_bytes(&sig_url).await?;
254
255    progress.report_started("Verifying ML-DSA signature...");
256    verify_signature(&archive_bytes, &sig_bytes)?;
257    progress.report_complete("Signature verified");
258
259    progress.report_started("Extracting archive...");
260    let extracted = if download_url.ends_with(".zip") {
261        extract_zip(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
262    } else {
263        extract_tar_gz(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
264    };
265    let binary_path = extracted.binary_path;
266
267    // Verify the extracted binary reports the expected version.
268    let actual_version = extract_version(&binary_path).await;
269    if let Ok(ref v) = actual_version {
270        if v != &check.latest_version {
271            return Err(Error::UpdateFailed(format!(
272                "version mismatch: expected {}, binary reports {v}",
273                check.latest_version
274            )));
275        }
276    }
277
278    replace_binary(&binary_path)?;
279
280    // Clean up is handled by tmp_dir Drop, but remove the archive explicitly
281    // since it can be large.
282    let _ = std::fs::remove_file(&archive_path);
283
284    Ok(UpdateResult {
285        previous_version: check.current_version.clone(),
286        new_version: check.latest_version.clone(),
287    })
288}
289
290/// Fetch the highest CLI release version eligible for `channel` from GitHub.
291///
292/// Lists all releases and finds the newest non-draft one whose tag starts with
293/// `ant-cli-v` and whose version is eligible for `channel`.
294async fn fetch_latest_cli_version(channel: UpgradeChannel) -> Result<String> {
295    // per_page=100 is the GitHub API maximum; covers repos with many release tags.
296    let url = format!("https://api.github.com/repos/{GITHUB_REPO}/releases?per_page=100");
297    let client = reqwest::Client::new();
298    let resp = client
299        .get(&url)
300        .header("User-Agent", "ant-cli")
301        .header("Accept", "application/vnd.github+json")
302        .send()
303        .await
304        .map_err(|e| Error::UpdateFailed(format!("failed to fetch releases: {e}")))?;
305
306    if !resp.status().is_success() {
307        return Err(Error::UpdateFailed(format!(
308            "GitHub API returned status {} when fetching releases",
309            resp.status()
310        )));
311    }
312
313    let releases: Vec<serde_json::Value> = resp
314        .json()
315        .await
316        .map_err(|e| Error::UpdateFailed(format!("failed to parse releases JSON: {e}")))?;
317
318    select_channel_version(&releases, channel).ok_or_else(|| {
319        Error::UpdateFailed(format!(
320            "no ant-cli release found on GitHub for the {channel} channel"
321        ))
322    })
323}
324
325/// Pick the highest `ant-cli-v*` version from a GitHub releases payload that is eligible
326/// for `channel`.
327///
328/// Draft releases are skipped. GitHub's `prerelease` flag is deliberately not consulted:
329/// the tag's own semver pre-release component is the authority, so the rule here is the
330/// same one the node applies to its own upgrades. Filtering on the flag instead is what
331/// made a beta `ant` self-update back onto stable.
332fn select_channel_version(
333    releases: &[serde_json::Value],
334    channel: UpgradeChannel,
335) -> Option<String> {
336    let mut best: Option<semver::Version> = None;
337
338    for release in releases {
339        if release["draft"].as_bool().unwrap_or(false) {
340            continue;
341        }
342
343        let tag = release["tag_name"].as_str().unwrap_or_default();
344        let Some(version_str) = tag.strip_prefix(TAG_PREFIX) else {
345            continue;
346        };
347        let Ok(version) = semver::Version::parse(version_str) else {
348            continue;
349        };
350
351        if !version_matches_channel(&version, channel) {
352            continue;
353        }
354
355        if best.as_ref().is_none_or(|b| version > *b) {
356            best = Some(version);
357        }
358    }
359
360    best.map(|v| v.to_string())
361}
362
363/// Download a release archive to a temp directory, streaming to disk.
364///
365/// Returns the path to the downloaded file and the raw bytes (needed for
366/// signature verification before extraction).
367async fn download_archive(
368    url: &str,
369    tmp_dir: &Path,
370    progress: &dyn ProgressReporter,
371) -> Result<(PathBuf, Vec<u8>)> {
372    progress.report_started(&format!("Downloading {CLI_BINARY_NAME} from {url}"));
373
374    let client = reqwest::Client::new();
375    let resp = client
376        .get(url)
377        .header("User-Agent", "ant-cli")
378        .send()
379        .await
380        .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
381
382    if !resp.status().is_success() {
383        return Err(Error::UpdateFailed(format!(
384            "download returned status {}",
385            resp.status()
386        )));
387    }
388
389    let total_size = resp.content_length().unwrap_or(0);
390    let mut downloaded: u64 = 0;
391
392    let tmp_archive = tmp_dir.join(".download.tmp");
393    let mut tmp_file = std::fs::File::create(&tmp_archive)
394        .map_err(|e| Error::UpdateFailed(format!("failed to create temp file: {e}")))?;
395
396    let mut stream = resp.bytes_stream();
397    while let Some(chunk) = stream.next().await {
398        let chunk =
399            chunk.map_err(|e| Error::UpdateFailed(format!("download stream error: {e}")))?;
400        downloaded += chunk.len() as u64;
401        std::io::Write::write_all(&mut tmp_file, &chunk)
402            .map_err(|e| Error::UpdateFailed(format!("failed to write temp file: {e}")))?;
403        progress.report_progress(downloaded, total_size);
404    }
405    drop(tmp_file);
406
407    progress.report_complete("Download complete");
408
409    let bytes = std::fs::read(&tmp_archive)
410        .map_err(|e| Error::UpdateFailed(format!("failed to read temp file: {e}")))?;
411
412    Ok((tmp_archive, bytes))
413}
414
415/// Download a small file (e.g., a `.sig` signature) into memory.
416async fn download_bytes(url: &str) -> Result<Vec<u8>> {
417    let client = reqwest::Client::new();
418    let resp = client
419        .get(url)
420        .header("User-Agent", "ant-cli")
421        .send()
422        .await
423        .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
424
425    if !resp.status().is_success() {
426        return Err(Error::UpdateFailed(format!(
427            "download returned status {} for {url}",
428            resp.status()
429        )));
430    }
431
432    resp.bytes()
433        .await
434        .map(|b| b.to_vec())
435        .map_err(|e| Error::UpdateFailed(format!("failed to read response body: {e}")))
436}
437
438/// Verify the ML-DSA-65 signature on an archive using the embedded release key.
439fn verify_signature(archive_bytes: &[u8], signature_bytes: &[u8]) -> Result<()> {
440    if signature_bytes.len() != SIGNATURE_SIZE {
441        return Err(Error::UpdateFailed(format!(
442            "invalid signature size: expected {SIGNATURE_SIZE}, got {}",
443            signature_bytes.len()
444        )));
445    }
446
447    let public_key = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, RELEASE_SIGNING_KEY)
448        .map_err(|e| Error::UpdateFailed(format!("invalid embedded release key: {e}")))?;
449
450    let sig = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, signature_bytes)
451        .map_err(|e| Error::UpdateFailed(format!("invalid signature format: {e}")))?;
452
453    let dsa = ml_dsa_65();
454    let valid = dsa
455        .verify_with_context(&public_key, archive_bytes, &sig, SIGNING_CONTEXT)
456        .map_err(|e| Error::UpdateFailed(format!("signature verification error: {e}")))?;
457
458    if valid {
459        Ok(())
460    } else {
461        Err(Error::UpdateFailed(
462            "signature verification failed: archive may be corrupted or tampered".to_string(),
463        ))
464    }
465}
466
467/// Extract the version string from a binary by running `<binary> --version`.
468async fn extract_version(binary_path: &Path) -> Result<String> {
469    let mut cmd = tokio::process::Command::new(binary_path);
470    cmd.arg("--version");
471    // CREATE_NO_WINDOW: prevents Windows from allocating a console window for
472    // the console-subsystem child binary. Without this, every version probe
473    // flashes a window — visible as "ghost flashes" in GUI consumers.
474    #[cfg(windows)]
475    {
476        const CREATE_NO_WINDOW: u32 = 0x08000000;
477        cmd.creation_flags(CREATE_NO_WINDOW);
478    }
479    let output = cmd.output().await.map_err(|e| {
480        Error::UpdateFailed(format!(
481            "failed to run {} --version: {e}",
482            binary_path.display()
483        ))
484    })?;
485
486    if !output.status.success() {
487        return Err(Error::UpdateFailed(format!(
488            "{} --version exited with status {}",
489            binary_path.display(),
490            output.status
491        )));
492    }
493
494    let stdout = String::from_utf8_lossy(&output.stdout);
495    Ok(parse_version_from_stdout(&stdout).to_string())
496}
497
498/// Extract the version token from `--version` stdout.
499///
500/// Only the first line is inspected: historically `ant --version` was a single
501/// line like `ant 0.1.4`, but a richer multi-line long-version string ending
502/// with `License: MIT or Apache-2.0` shipped briefly and caused this parser —
503/// in 0.1.2 through 0.1.4 — to pick up `Apache-2.0` as the version. Scoping
504/// to the first line keeps us compatible with both forms.
505fn parse_version_from_stdout(stdout: &str) -> &str {
506    stdout
507        .lines()
508        .next()
509        .unwrap_or_default()
510        .split_whitespace()
511        .last()
512        .unwrap_or("unknown")
513}
514
515/// Replace the current executable with the new binary.
516///
517/// Uses the `self_replace` crate which handles platform-specific nuances,
518/// particularly on Windows where the running executable is locked.
519fn replace_binary(new_binary: &Path) -> Result<()> {
520    self_replace::self_replace(new_binary)
521        .map_err(|e| Error::UpdateFailed(format!("failed to replace binary: {e}")))?;
522    Ok(())
523}
524
525/// Build the download URL for a given version.
526///
527/// Tag format: `ant-cli-v{version}`
528/// Asset format: `ant-{version}-{target_triple}.{ext}`
529pub fn build_download_url(version: &str) -> Result<String> {
530    let asset_name = cli_platform_asset_name(version)?;
531    Ok(format!(
532        "https://github.com/{GITHUB_REPO}/releases/download/{TAG_PREFIX}{version}/{asset_name}"
533    ))
534}
535
536/// Returns the platform-specific archive asset name for the CLI binary.
537///
538/// Matches the naming convention from CI: `ant-{version}-{target_triple}.{ext}`
539fn cli_platform_asset_name(version: &str) -> Result<String> {
540    let target_triple = if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
541        "x86_64-unknown-linux-musl"
542    } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
543        "aarch64-unknown-linux-musl"
544    } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
545        "x86_64-apple-darwin"
546    } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
547        "aarch64-apple-darwin"
548    } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
549        "x86_64-pc-windows-msvc"
550    } else {
551        return Err(Error::UpdateFailed(format!(
552            "unsupported platform: {}-{}",
553            std::env::consts::OS,
554            std::env::consts::ARCH
555        )));
556    };
557
558    let ext = if cfg!(target_os = "windows") {
559        "zip"
560    } else {
561        "tar.gz"
562    };
563
564    Ok(format!("ant-{version}-{target_triple}.{ext}"))
565}
566
567fn parse_version(version: &str) -> Result<semver::Version> {
568    let cleaned = version.strip_prefix('v').unwrap_or(version);
569    semver::Version::parse(cleaned)
570        .map_err(|e| Error::UpdateFailed(format!("invalid version '{version}': {e}")))
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576
577    #[test]
578    fn parse_version_from_stdout_single_line() {
579        assert_eq!(parse_version_from_stdout("ant 0.1.4"), "0.1.4");
580        assert_eq!(parse_version_from_stdout("ant 0.1.4\n"), "0.1.4");
581    }
582
583    #[test]
584    fn parse_version_from_stdout_multi_line_license_trailer() {
585        // This is the exact shape that broke 0.1.2–0.1.4 self-update.
586        let stdout = "ant 0.1.4\n\
587            Autonomi network client\n\
588            \n\
589            Repository: https://github.com/WithAutonomi/ant-client\n\
590            License:    MIT or Apache-2.0\n";
591        assert_eq!(parse_version_from_stdout(stdout), "0.1.4");
592    }
593
594    #[test]
595    fn parse_version_from_stdout_empty() {
596        assert_eq!(parse_version_from_stdout(""), "unknown");
597    }
598
599    #[test]
600    fn parse_version_from_stdout_blank_first_line() {
601        assert_eq!(parse_version_from_stdout("\nant 0.1.4\n"), "unknown");
602    }
603
604    #[test]
605    fn parse_version_from_stdout_leading_whitespace() {
606        assert_eq!(parse_version_from_stdout("   ant 0.1.4\n"), "0.1.4");
607    }
608
609    #[test]
610    fn parse_version_valid() {
611        assert!(parse_version("1.2.3").is_ok());
612        assert!(parse_version("v1.2.3").is_ok());
613        assert!(parse_version("0.1.0").is_ok());
614    }
615
616    #[test]
617    fn parse_version_invalid() {
618        assert!(parse_version("not-a-version").is_err());
619        assert!(parse_version("").is_err());
620    }
621
622    #[test]
623    fn version_comparison() {
624        let v1 = parse_version("0.1.0").unwrap();
625        let v2 = parse_version("0.2.0").unwrap();
626        assert!(v2 > v1);
627
628        let v3 = parse_version("1.0.0").unwrap();
629        assert!(v3 > v2);
630
631        let same = parse_version("0.1.0").unwrap();
632        assert_eq!(v1, same);
633    }
634
635    #[test]
636    fn check_result_no_update() {
637        let check = UpdateCheck {
638            current_version: "1.0.0".to_string(),
639            latest_version: "1.0.0".to_string(),
640            update_available: false,
641            download_url: None,
642            channel: UpgradeChannel::Stable,
643        };
644        assert!(!check.update_available);
645        assert!(check.download_url.is_none());
646    }
647
648    #[test]
649    fn check_result_with_update() {
650        let check = UpdateCheck {
651            current_version: "0.1.0".to_string(),
652            latest_version: "0.2.0".to_string(),
653            update_available: true,
654            download_url: Some("https://example.com/ant.tar.gz".to_string()),
655            channel: UpgradeChannel::Stable,
656        };
657        assert!(check.update_available);
658        assert!(check.download_url.is_some());
659    }
660
661    #[test]
662    fn force_populates_download_url() {
663        let mut check = UpdateCheck {
664            current_version: "1.0.0".to_string(),
665            latest_version: "1.0.0".to_string(),
666            update_available: false,
667            download_url: None,
668            channel: UpgradeChannel::Stable,
669        };
670        check.force().unwrap();
671        assert!(check.update_available);
672        assert!(check.download_url.is_some());
673    }
674
675    #[test]
676    fn platform_asset_name_format() {
677        let name = cli_platform_asset_name("1.2.3").unwrap();
678        assert!(name.starts_with("ant-1.2.3-"));
679        assert!(
680            name.ends_with(".tar.gz") || name.ends_with(".zip"),
681            "unexpected extension: {name}"
682        );
683    }
684
685    #[test]
686    fn build_download_url_format() {
687        let url = build_download_url("1.2.3").unwrap();
688        assert!(url.starts_with(
689            "https://github.com/WithAutonomi/ant-client/releases/download/ant-cli-v1.2.3/ant-1.2.3-"
690        ));
691        assert!(url.ends_with(".tar.gz") || url.ends_with(".zip"));
692    }
693
694    #[test]
695    fn update_check_serializes() {
696        let check = UpdateCheck {
697            current_version: "0.1.0".to_string(),
698            latest_version: "0.2.0".to_string(),
699            update_available: true,
700            download_url: Some("https://example.com/ant.tar.gz".to_string()),
701            channel: UpgradeChannel::Stable,
702        };
703        let json = serde_json::to_string(&check).unwrap();
704        let deserialized: UpdateCheck = serde_json::from_str(&json).unwrap();
705        assert_eq!(deserialized.current_version, "0.1.0");
706        assert!(deserialized.update_available);
707    }
708
709    #[test]
710    fn verify_signature_rejects_wrong_size() {
711        let result = verify_signature(b"some archive data", &[0u8; 100]);
712        assert!(result.is_err());
713        let err = result.unwrap_err().to_string();
714        assert!(err.contains("invalid signature size"), "got: {err}");
715    }
716
717    #[test]
718    fn verify_signature_rejects_invalid_signature() {
719        let invalid_sig = vec![0u8; SIGNATURE_SIZE];
720        let result = verify_signature(b"some archive data", &invalid_sig);
721        assert!(result.is_err());
722    }
723
724    #[test]
725    fn verify_signature_valid_roundtrip() {
726        let dsa = ml_dsa_65();
727        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
728        let archive = b"fake archive content for testing";
729
730        let sig = dsa
731            .sign_with_context(&secret_key, archive, SIGNING_CONTEXT)
732            .unwrap();
733
734        // Use the _with_key variant for testing (can't use embedded key)
735        let parsed_sig =
736            MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig.to_bytes()).unwrap();
737        let valid = dsa
738            .verify_with_context(&public_key, archive, &parsed_sig, SIGNING_CONTEXT)
739            .unwrap();
740        assert!(valid);
741    }
742
743    fn release(tag: &str) -> serde_json::Value {
744        serde_json::json!({ "tag_name": tag, "draft": false })
745    }
746
747    #[test]
748    fn stable_selects_the_highest_final_release() {
749        let releases = [
750            release("ant-cli-v0.3.3"),
751            release("ant-cli-v0.3.4-beta.1"),
752            release("ant-cli-v0.3.4-rc.1"),
753        ];
754
755        assert_eq!(
756            select_channel_version(&releases, UpgradeChannel::Stable),
757            Some("0.3.3".to_string())
758        );
759    }
760
761    #[test]
762    fn beta_selects_the_beta_over_the_rc() {
763        let releases = [
764            release("ant-cli-v0.3.3"),
765            release("ant-cli-v0.3.4-beta.1"),
766            release("ant-cli-v0.3.4-rc.1"),
767        ];
768
769        assert_eq!(
770            select_channel_version(&releases, UpgradeChannel::Beta),
771            Some("0.3.4-beta.1".to_string())
772        );
773    }
774
775    /// The `prerelease` flag is not consulted: a beta release carries it, and skipping on it
776    /// is exactly what dragged a beta `ant` back onto stable.
777    #[test]
778    fn the_github_prerelease_flag_does_not_hide_a_beta() {
779        let mut beta = release("ant-cli-v0.3.4-beta.1");
780        beta["prerelease"] = serde_json::Value::Bool(true);
781        let releases = [release("ant-cli-v0.3.3"), beta];
782
783        assert_eq!(
784            select_channel_version(&releases, UpgradeChannel::Beta),
785            Some("0.3.4-beta.1".to_string())
786        );
787    }
788
789    #[test]
790    fn drafts_and_foreign_tags_are_skipped() {
791        let mut draft = release("ant-cli-v0.4.0");
792        draft["draft"] = serde_json::Value::Bool(true);
793        let releases = [
794            release("ant-core-v0.9.0"),
795            release("v0.5.0"),
796            draft,
797            release("ant-cli-v0.3.3"),
798        ];
799
800        assert_eq!(
801            select_channel_version(&releases, UpgradeChannel::Stable),
802            Some("0.3.3".to_string())
803        );
804    }
805
806    #[test]
807    fn no_eligible_release_yields_none() {
808        let releases = [release("ant-cli-v0.3.4-rc.1")];
809
810        assert_eq!(
811            select_channel_version(&releases, UpgradeChannel::Beta),
812            None
813        );
814    }
815
816    #[test]
817    fn channel_is_inferred_from_the_running_version() {
818        assert_eq!(channel_for_version("0.3.4-beta.1"), UpgradeChannel::Beta);
819        assert_eq!(channel_for_version("0.3.4-beta"), UpgradeChannel::Beta);
820        assert_eq!(channel_for_version("0.3.3"), UpgradeChannel::Stable);
821        assert_eq!(channel_for_version("0.3.4-rc.1"), UpgradeChannel::Stable);
822        assert_eq!(channel_for_version("0.3.4-alpha.1"), UpgradeChannel::Stable);
823        // `betamax` must not be inferred as beta by a prefix match.
824        assert_eq!(
825            channel_for_version("0.3.4-betamax.1"),
826            UpgradeChannel::Stable
827        );
828        assert_eq!(channel_for_version("not-a-version"), UpgradeChannel::Stable);
829    }
830
831    /// A `--json` payload written before the channel field existed still deserializes.
832    #[test]
833    fn update_check_deserializes_without_a_channel_field() {
834        let json = r#"{"current_version":"0.1.0","latest_version":"0.2.0",
835            "update_available":true,"download_url":null}"#;
836        let check: UpdateCheck = serde_json::from_str(json).unwrap();
837        assert_eq!(check.channel, UpgradeChannel::Stable);
838    }
839}