Skip to content

Commit 494be8e

Browse files
committed
feat: add upgrade delay setting for upgrade --all
1 parent d567b47 commit 494be8e

16 files changed

Lines changed: 365 additions & 0 deletions

File tree

doc/ReleaseNotes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ match criteria that factor into the result ordering. This will prevent them from
2828

2929
## Minor Features
3030

31+
### Upgrade delay for `winget upgrade --all`
32+
33+
Added the `installBehavior.upgradeDelayInDays` user setting to delay `winget upgrade --all` until an upgrade's `ReleaseDate` is at least N days old. This helps reduce exposure to newly published updates that may be part of a supply chain attack.
34+
3135
### Preserve installer arguments across export and import
3236

3337
`winget export` now captures the `--override` and `--custom` arguments that were used when a package was originally installed and saves them into the export file. When subsequently running `winget import`, those values are automatically re-applied during installation — `--override` replaces all installer arguments and `--custom` appends extra switches — so packages can be reinstalled with the same customizations without any manual intervention. Both fields are optional and independent of each other; packages without stored installer arguments are unaffected.

doc/Settings.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,18 @@ The `maxResumes` setting determines the maximum number of times that a command m
218218

219219
> Note: [The resume behavior is an experimental feature.](#resume)
220220
221+
### Upgrade delay
222+
223+
The `upgradeDelayInDays` setting delays upgrades when using `winget upgrade --all` by skipping upgrades whose manifest `ReleaseDate` is more recent than the configured age. The default value is 0 (disabled).
224+
225+
```json
226+
"installBehavior": {
227+
"upgradeDelayInDays": 14
228+
},
229+
```
230+
231+
> Note: If the package manifest does not include a valid `ReleaseDate`, the upgrade will be skipped when this setting is enabled. To upgrade anyway, use `winget upgrade <package>` which does not consider this setting, or use `winget upgrade --all --force` which will ignore the age of the upgrade.
232+
221233
## Uninstall Behavior
222234

223235
The `uninstallBehavior` settings affect the default behavior of uninstalling (where applicable) packages.

schemas/JSON/settings/settings.schema.0.2.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@
215215
"default": 3,
216216
"minimum": 1
217217
},
218+
"upgradeDelayInDays": {
219+
"description": "Minimum number of days since the manifest ReleaseDate before winget upgrade --all applies the upgrade. Set to 0 to disable.",
220+
"type": "integer",
221+
"default": 0,
222+
"minimum": 0,
223+
"maximum": 3650
224+
},
218225
"archiveExtractionMethod": {
219226
"description": "Controls the behavior how the installer extracts archives. The current two supported values are 'shellApi' and 'tar'. 'shellApi' uses the Windows Shell API to extract archives. 'tar' uses the tar command to extract archives.",
220227
"type": "string",

src/AppInstallerCLICore/Resources.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ namespace AppInstaller::CLI::Resource
796796
WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandShortDescription);
797797
WINGET_DEFINE_RESOURCE_STRINGID(UpgradeDifferentInstallTechnology);
798798
WINGET_DEFINE_RESOURCE_STRINGID(UpgradeDifferentInstallTechnologyInNewerVersions);
799+
WINGET_DEFINE_RESOURCE_STRINGID(UpgradeDelaySkippedCount);
799800
WINGET_DEFINE_RESOURCE_STRINGID(UpgradeInstallTechnologyMismatchCount);
800801
WINGET_DEFINE_RESOURCE_STRINGID(UpgradeIsPinned);
801802
WINGET_DEFINE_RESOURCE_STRINGID(UpgradePinnedByUserCount);

src/AppInstallerCLICore/Workflows/UpdateFlow.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <winget/ManifestComparator.h>
99
#include <winget/PinningData.h>
1010
#include <winget/PackageVersionSelection.h>
11+
#include <ctime>
1112

1213
using namespace AppInstaller::Repository;
1314
using namespace AppInstaller::Repository::Microsoft;
@@ -17,6 +18,75 @@ namespace AppInstaller::CLI::Workflow
1718
{
1819
namespace
1920
{
21+
std::optional<std::chrono::system_clock::time_point> TryParseISO8601Date(std::string_view value)
22+
{
23+
// YYYY-MM-DD is the expected format
24+
if (value.size() < 10)
25+
{
26+
return {};
27+
}
28+
29+
auto toInt = [](std::string_view sv) -> std::optional<int>
30+
{
31+
int result = 0;
32+
for (char c : sv)
33+
{
34+
if (c < '0' || c > '9')
35+
{
36+
return {};
37+
}
38+
result = (result * 10) + (c - '0');
39+
}
40+
return result;
41+
};
42+
43+
auto year = toInt(value.substr(0, 4));
44+
auto month = toInt(value.substr(5, 2));
45+
auto day = toInt(value.substr(8, 2));
46+
47+
if (!year || !month || !day || value[4] != '-' || value[7] != '-')
48+
{
49+
return {};
50+
}
51+
52+
if (*month < 1 || *month > 12 || *day < 1 || *day > 31)
53+
{
54+
return {};
55+
}
56+
57+
std::tm tm{};
58+
tm.tm_year = *year - 1900;
59+
tm.tm_mon = *month - 1;
60+
tm.tm_mday = *day;
61+
tm.tm_hour = 0;
62+
tm.tm_min = 0;
63+
tm.tm_sec = 0;
64+
65+
time_t utcTime = _mkgmtime(&tm);
66+
if (utcTime == static_cast<time_t>(-1))
67+
{
68+
return {};
69+
}
70+
71+
return std::chrono::system_clock::from_time_t(utcTime);
72+
}
73+
74+
bool IsUpgradeEligibleByDelay(std::string_view releaseDate, std::chrono::hours upgradeDelay)
75+
{
76+
if (upgradeDelay <= std::chrono::hours{ 0 })
77+
{
78+
return true;
79+
}
80+
81+
auto parsed = TryParseISO8601Date(releaseDate);
82+
if (!parsed)
83+
{
84+
return false;
85+
}
86+
87+
return (std::chrono::system_clock::now() - *parsed) >= upgradeDelay;
88+
}
89+
2090
bool IsUpdateVersionAvailable(const Utility::Version& installedVersion, const Utility::Version& updateVersion)
2191
{
2292
return installedVersion < updateVersion;
@@ -217,6 +287,9 @@ namespace AppInstaller::CLI::Workflow
217287
int packagesWithUnknownVersionSkipped = 0;
218288
int packagesThatRequireExplicitSkipped = 0;
219289
int packagesSkippedInstallTechnologyMismatch = 0;
290+
int packagesSkippedByUpgradeDelay = 0;
291+
const auto upgradeDelay = Settings::User().Get<Settings::Setting::UpgradeDelayInDays>();
292+
const bool ignoreUpgradeDelay = context.Args.Contains(Execution::Args::Type::Force);
220293

221294
for (const auto& match : matches)
222295
{
@@ -256,6 +329,36 @@ namespace AppInstaller::CLI::Workflow
256329
continue;
257330
}
258331

332+
if (!ignoreUpgradeDelay && upgradeDelay > std::chrono::hours{ 0 })
333+
{
334+
std::string_view releaseDate;
335+
if (updateContext.Contains(Execution::Data::Installer))
336+
{
337+
const auto& installer = updateContext.Get<Execution::Data::Installer>();
338+
if (installer.has_value() && !installer->ReleaseDate.empty())
339+
{
340+
releaseDate = installer->ReleaseDate;
341+
}
342+
}
343+
344+
if (releaseDate.empty() && updateContext.Contains(Execution::Data::Manifest))
345+
{
346+
const auto& manifest = updateContext.Get<Execution::Data::Manifest>();
347+
if (!manifest.DefaultInstallerInfo.ReleaseDate.empty())
348+
{
349+
releaseDate = manifest.DefaultInstallerInfo.ReleaseDate;
350+
}
351+
}
352+
353+
if (!IsUpgradeEligibleByDelay(releaseDate, upgradeDelay))
354+
{
355+
AICLI_LOG(CLI, Info, << "Skipping " << match.Package->GetProperty(PackageProperty::Id)
356+
<< " as its selected upgrade does not meet the upgrade delay requirement");
357+
++packagesSkippedByUpgradeDelay;
358+
continue;
359+
}
360+
}
361+
259362
// Filter out packages that require explicit upgrade.
260363
// User-defined pins are handled when selecting the version to use.
261364
auto installedMetadata = updateContext.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata();
@@ -314,6 +417,12 @@ namespace AppInstaller::CLI::Workflow
314417
AICLI_LOG(CLI, Info, << packagesSkippedInstallTechnologyMismatch << " package(s) skipped due to install technology mismatch");
315418
context.Reporter.Info() << Resource::String::UpgradeInstallTechnologyMismatchCount(packagesSkippedInstallTechnologyMismatch) << std::endl;
316419
}
420+
421+
if (packagesSkippedByUpgradeDelay > 0)
422+
{
423+
AICLI_LOG(CLI, Info, << packagesSkippedByUpgradeDelay << " package(s) skipped due to upgrade delay setting");
424+
context.Reporter.Info() << Resource::String::UpgradeDelaySkippedCount(packagesSkippedByUpgradeDelay) << std::endl;
425+
}
317426
}
318427

319428
void SelectSinglePackageVersionForInstallOrUpgrade::operator()(Execution::Context& context) const

src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,10 @@ Please specify one of them using the --source option to proceed.</value>
13891389
<value>{0} package(s) are pinned and need to be explicitly upgraded.</value>
13901390
<comment>{Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages that require explicit upgrades.</comment>
13911391
</data>
1392+
<data name="UpgradeDelaySkippedCount" xml:space="preserve">
1393+
<value>{0} package(s) were skipped due to the configured upgrade delay. To upgrade them anyway, upgrade them individually or use --force.</value>
1394+
<comment>{Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages skipped for this reason during winget upgrade --all.</comment>
1395+
</data>
13921396
<data name="InvalidArgumentWithoutQueryError" xml:space="preserve">
13931397
<value>The arguments provided can only be used with a query.</value>
13941398
</data>

src/AppInstallerCLITests/AppInstallerCLITests.vcxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,18 @@
727727
<CopyFileToFolders Include="TestData\UpdateFlowTest_Exe.yaml">
728728
<DeploymentContent>true</DeploymentContent>
729729
</CopyFileToFolders>
730+
<CopyFileToFolders Include="TestData\UpgradeDelayTest_Exe_Eligible_Install.yaml">
731+
<DeploymentContent>true</DeploymentContent>
732+
</CopyFileToFolders>
733+
<CopyFileToFolders Include="TestData\UpgradeDelayTest_Exe_Eligible_Update.yaml">
734+
<DeploymentContent>true</DeploymentContent>
735+
</CopyFileToFolders>
736+
<CopyFileToFolders Include="TestData\UpgradeDelayTest_Exe_TooRecent_Install.yaml">
737+
<DeploymentContent>true</DeploymentContent>
738+
</CopyFileToFolders>
739+
<CopyFileToFolders Include="TestData\UpgradeDelayTest_Exe_TooRecent_Update.yaml">
740+
<DeploymentContent>true</DeploymentContent>
741+
</CopyFileToFolders>
730742
<CopyFileToFolders Include="TestData\UpdateFlowTest_Exe_ARPInstallerType.yaml">
731743
<DeploymentContent>true</DeploymentContent>
732744
</CopyFileToFolders>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.1.0.schema.json
2+
3+
PackageIdentifier: AppInstallerCliTest.TestExeDelayEligible
4+
PackageVersion: 1.0.0.0
5+
PackageLocale: en-US
6+
Publisher: Microsoft Corporation
7+
PackageName: AppInstaller Test Exe Installer (Delay Eligible)
8+
License: Test
9+
InstallerType: exe
10+
InstallerSwitches:
11+
Upgrade: /update
12+
SilentWithProgress: /silentwithprogress
13+
Silent: /silence
14+
Installers:
15+
- Architecture: x64
16+
InstallerUrl: https://ThisIsNotUsed
17+
InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
18+
19+
ManifestType: singleton
20+
ManifestVersion: 1.1.0
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.1.0.schema.json
2+
3+
PackageIdentifier: AppInstallerCliTest.TestExeDelayEligible
4+
PackageVersion: 2.0.0.0
5+
PackageLocale: en-US
6+
Publisher: Microsoft Corporation
7+
PackageName: AppInstaller Test Exe Installer (Delay Eligible)
8+
License: Test
9+
ReleaseDate: 2000-01-01
10+
InstallerType: exe
11+
InstallerSwitches:
12+
Upgrade: /update
13+
SilentWithProgress: /silentwithprogress
14+
Silent: /silence
15+
Installers:
16+
- Architecture: x64
17+
InstallerUrl: https://ThisIsNotUsed
18+
InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
19+
20+
ManifestType: singleton
21+
ManifestVersion: 1.1.0
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.1.0.schema.json
2+
3+
PackageIdentifier: AppInstallerCliTest.TestExeDelayTooRecent
4+
PackageVersion: 1.0.0.0
5+
PackageLocale: en-US
6+
Publisher: Microsoft Corporation
7+
PackageName: AppInstaller Test Exe Installer (Delay Too Recent)
8+
License: Test
9+
InstallerType: exe
10+
InstallerSwitches:
11+
Upgrade: /update
12+
SilentWithProgress: /silentwithprogress
13+
Silent: /silence
14+
Installers:
15+
- Architecture: x64
16+
InstallerUrl: https://ThisIsNotUsed
17+
InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
18+
19+
ManifestType: singleton
20+
ManifestVersion: 1.1.0

0 commit comments

Comments
 (0)