@sous-io/sous 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -107,10 +107,12 @@ import { RecipeStore } from "./store/recipe-store.js";
107
107
  import type { RecipeStoreLike, StoreKey } from "./store/contract.js";
108
108
  import { resolveStoreSettings } from "./store/settings.js";
109
109
  import {
110
+ NEWER_VERSION_CHECK_TIMEOUT_MS,
110
111
  effectiveRangeForHolders,
111
112
  findNewerInRange,
112
113
  recordUpstreamCheck,
113
114
  shouldCheckUpstream,
115
+ withDeadline,
114
116
  } from "./freshness.js";
115
117
  import { REPO_NAME_PATTERN } from "./formats/patterns.js";
116
118
  import {
@@ -388,6 +390,17 @@ export type UpstreamCheckReport = {
388
390
  updated: Array<{ key: string; from: string; to: string }>;
389
391
  /** Repositories whose check failed; the last good answer still stands. */
390
392
  failed: Array<{ repo: string; reason: string }>;
393
+ /**
394
+ * Locked recipes with a newer version inside the range their holders
395
+ * declared, in a repository that does not prefer newer versions. Nothing
396
+ * moved; this is only what a build tells the person running it.
397
+ */
398
+ newer: Array<{ key: string; repo: string; from: string; to: string }>;
399
+ /**
400
+ * Repositories sous looked at only to find newer versions, whose look failed
401
+ * or ran out of time. Kept quiet: the cached index answered instead.
402
+ */
403
+ unchecked: Array<{ repo: string; reason: string }>;
391
404
  };
392
405
 
393
406
  // --- The service --------------------------------------------------------------------------------
@@ -1828,16 +1841,28 @@ export class SubscriptionService {
1828
1841
  }
1829
1842
 
1830
1843
  /**
1831
- * Looks upstream for the repositories that prefer a newer in-range version,
1832
- * and moves the lockfile to it when there is one. A failed check never breaks
1833
- * a build: it is warned about and the last good answer stands.
1844
+ * Looks upstream for every repository the lockfile pins something from, on
1845
+ * the freshness window. A repository that prefers a newer in-range version
1846
+ * moves the lockfile to it; every other repository is only read, and a newer
1847
+ * in-range version it publishes is reported in `newer` without moving any
1848
+ * pin. A failed check never breaks a build: for a repository that prefers
1849
+ * newer versions it is warned about, and for the others it is quiet, capped
1850
+ * at a short deadline, and the cached index answers instead.
1834
1851
  *
1835
- * @param options - Whether to check regardless of the freshness window, and which window to use.
1852
+ * @param options - Whether to check regardless of the freshness window, which
1853
+ * window to use, and how long to wait for a repository checked only for the
1854
+ * newer-version report.
1836
1855
  */
1837
1856
  async checkUpstream(
1838
- options: { force?: boolean; freshnessSeconds?: number } = {}
1857
+ options: { force?: boolean; freshnessSeconds?: number; newerCheckTimeoutMs?: number } = {}
1839
1858
  ): Promise<UpstreamCheckReport> {
1840
- const report: UpstreamCheckReport = { checked: [], updated: [], failed: [] };
1859
+ const report: UpstreamCheckReport = {
1860
+ checked: [],
1861
+ updated: [],
1862
+ failed: [],
1863
+ newer: [],
1864
+ unchecked: [],
1865
+ };
1841
1866
  const lock = this.lock.read();
1842
1867
  if (Object.keys(lock.recipes).length === 0) return report;
1843
1868
 
@@ -1850,19 +1875,32 @@ export class SubscriptionService {
1850
1875
  const recipes: Record<string, LockedRecipe> = { ...lock.recipes };
1851
1876
 
1852
1877
  for (const repoName of Object.keys(lock.repos).sort()) {
1853
- if (!this.prefersNewer(repoName, repos[repoName], lock, subscriptions)) continue;
1854
-
1855
1878
  const identity = this.identityForRepo(repoName, lock);
1856
1879
  if (identity === undefined) continue;
1857
1880
 
1881
+ const prefersNewer = this.prefersNewer(repoName, repos[repoName], lock, subscriptions);
1858
1882
  const meta = this.indexCache.readMeta(identity);
1859
1883
  const due = shouldCheckUpstream({
1860
1884
  ...(meta?.lastCheckedAt === undefined ? {} : { lastCheckedAt: meta.lastCheckedAt }),
1861
1885
  freshnessSeconds,
1862
- alwaysPull: true,
1886
+ alwaysPull: prefersNewer,
1863
1887
  ...(options.force === undefined ? {} : { force: options.force }),
1864
1888
  now: this.now(),
1865
1889
  });
1890
+
1891
+ if (!prefersNewer) {
1892
+ await this.reportNewerVersions({
1893
+ repoName,
1894
+ identity,
1895
+ due,
1896
+ lock,
1897
+ subscriptions,
1898
+ report,
1899
+ timeoutMs: options.newerCheckTimeoutMs ?? NEWER_VERSION_CHECK_TIMEOUT_MS,
1900
+ });
1901
+ continue;
1902
+ }
1903
+
1866
1904
  if (!due) continue;
1867
1905
 
1868
1906
  report.checked.push(repoName);
@@ -1938,6 +1976,97 @@ export class SubscriptionService {
1938
1976
  return report;
1939
1977
  }
1940
1978
 
1979
+ /**
1980
+ * The newer-version report for one repository that does not prefer newer
1981
+ * versions: reads its index (from upstream when the freshness window has
1982
+ * lapsed, within a short deadline, and from the cache otherwise) and records
1983
+ * every locked recipe from it that has a newer version in range. Nothing
1984
+ * moves, and nothing here is ever an error: a check that fails or runs out of
1985
+ * time is noted in `unchecked` and the cached index answers instead.
1986
+ *
1987
+ * @param input - The repository, whether a check is due, and the report to fill.
1988
+ */
1989
+ private async reportNewerVersions(input: {
1990
+ repoName: string;
1991
+ identity: string;
1992
+ due: boolean;
1993
+ lock: Lockfile;
1994
+ subscriptions: Record<string, SubscriptionEntry>;
1995
+ report: UpstreamCheckReport;
1996
+ timeoutMs: number;
1997
+ }): Promise<void> {
1998
+ const { repoName, identity, lock, report } = input;
1999
+ let index: IndexFile | undefined;
2000
+
2001
+ if (input.due) {
2002
+ report.checked.push(repoName);
2003
+ const repo = this.currentRepos()[repoName];
2004
+ const url = repo?.url ?? lock.repos[repoName]!.url;
2005
+ try {
2006
+ index = (
2007
+ await withDeadline(
2008
+ (signal) =>
2009
+ this.indexCache.refresh(identity, {
2010
+ url,
2011
+ label: repoName,
2012
+ ...(repo?.provider === undefined ? {} : { provider: repo.provider }),
2013
+ signal,
2014
+ }),
2015
+ input.timeoutMs
2016
+ )
2017
+ ).index;
2018
+ } catch (error) {
2019
+ report.unchecked.push({ repo: repoName, reason: describeError(error) });
2020
+ }
2021
+ // Recorded whatever happened, so an unreachable host costs one short wait
2022
+ // per freshness window rather than one per build.
2023
+ recordUpstreamCheck(this.indexCache, identity, this.now());
2024
+ }
2025
+
2026
+ index ??= this.indexCache.readCached(identity);
2027
+ if (index === undefined) return;
2028
+
2029
+ for (const [key, entry] of Object.entries(lock.recipes).sort(([a], [b]) =>
2030
+ a < b ? -1 : a > b ? 1 : 0
2031
+ )) {
2032
+ if (entry.repo !== repoName) continue;
2033
+ const range = this.effectiveRangeFor(key, entry, input.subscriptions);
2034
+ if (range === undefined) continue;
2035
+ const subscription =
2036
+ input.subscriptions[key] ?? input.subscriptions[key.split("/")[0]!];
2037
+
2038
+ const newer = findNewerInRange({
2039
+ index,
2040
+ key,
2041
+ lockedVersion: entry.version,
2042
+ ...(range === "*" ? {} : { range }),
2043
+ ...(subscription?.prerelease === undefined
2044
+ ? {}
2045
+ : { prerelease: subscription.prerelease }),
2046
+ });
2047
+ if (newer !== undefined) report.newer.push({ ...newer, repo: repoName });
2048
+ }
2049
+ }
2050
+
2051
+ /**
2052
+ * Reads one trusted repository's index from upstream, without writing it to
2053
+ * the cache: what a browsing command asked to read the latest uses. A failure
2054
+ * is raised, so the caller can fall back to the cached copy and say so.
2055
+ *
2056
+ * @param name - The repository's short name.
2057
+ */
2058
+ async upstreamIndex(name: string): Promise<IndexFile> {
2059
+ const repo = this.currentRepos()[name];
2060
+ if (repo === undefined) {
2061
+ throw new ConfigError(`This project trusts no repository called '${name}'.`);
2062
+ }
2063
+ const identity = this.identityOf(repo.url, repo.provider);
2064
+ return this.indexCache.fetchUpstream(identity, {
2065
+ url: repo.url,
2066
+ ...(repo.provider === undefined ? {} : { provider: repo.provider }),
2067
+ });
2068
+ }
2069
+
1941
2070
  /**
1942
2071
  * Everything a build needs done before it compiles: seed the packaged core
1943
2072
  * recipe, restore whatever else the store is missing, then look upstream for
@@ -151,3 +151,27 @@ export function nonInteractiveFlag() {
151
151
  default: false,
152
152
  });
153
153
  }
154
+
155
+ /**
156
+ * The two flags every browsing command takes, defined once so they read the
157
+ * same on each: `--latest` (also `--remote`) reads each repository's index from
158
+ * upstream rather than the cache, and `--installed` narrows what is shown to
159
+ * what this project's lockfile pins. They combine. The behavior lives in
160
+ * `lib/repos/catalog-inputs.ts` and `lib/repos/catalog.ts`.
161
+ */
162
+ export function browsingFlags() {
163
+ const latestAliases = ["remote"];
164
+ return {
165
+ latest: Flags.boolean({
166
+ aliases: latestAliases,
167
+ description:
168
+ "Read each repository's index from upstream instead of the cache, without " +
169
+ `saving it${aliasSuffix(latestAliases)}`,
170
+ default: false,
171
+ }),
172
+ installed: Flags.boolean({
173
+ description: "Show only what this project has installed, at the version its lockfile pins",
174
+ default: false,
175
+ }),
176
+ };
177
+ }