feat(update): updates and release downloads come from git.kof22.com (#171)
The in-app updater reads the project's releases on its own Gitea server through Velopack's GiteaSource, anonymously over HTTPS. The Scoop manifest (homepage, download, version check, auto-update), the Chocolatey package and the script that regenerates both for a release name the same server. - ADR 0009 amends ADR 0001. The threat model records where that trust is now placed: the updater checks hashes from the same feed and no signature, so whoever can publish on the server can deliver an update. T1 is accepted for 1.1.1, not mitigated. - Update-PackageManifests.ps1 refuses to write a manifest whose download does not answer, so a package cannot be pushed before its release is on the server. - RELEASING.md: what a release on git.kof22.com must contain, the prerelease flag, the ten-release window, and where a broken release is withdrawn. - PackagingManifestTests pin the auto-update URL, the hash URL, the release notes link and the version check exactly. Versions up to 1.1.0 look for updates on GitHub and are updated once by hand. Closes #170 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Signed-off-by: James Maes <james@kof22.com>
This commit was merged in pull request #171.
This commit is contained in:
1 parent
388b57ea94
commit
6696e37049
13 files changed
+187
-41
No files matched your search
@@ -29,7 +29,10 @@ param(
|
||||
|
||||
[string] $CliZipPath,
|
||||
|
||||
[string] $Sha256
|
||||
[string] $Sha256,
|
||||
|
||||
# Skips the check that the download is on the server (offline use, a rehearsal).
|
||||
[switch] $SkipDownloadCheck
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -76,7 +79,24 @@ if($Sha256 -notmatch '^[0-9a-f]{64}$')
|
||||
}
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||
$url = "https://github.com/KofTwentyTwo/gclo/releases/download/v$Version/gclo-cli-win-x64-$Version.zip"
|
||||
$url = "https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v$Version/gclo-cli-win-x64-$Version.zip"
|
||||
|
||||
################################################################################
|
||||
## The manifests name the download on git.kof22.com (ADR 0009). A package that ##
|
||||
## points at a file that is not there yet fails for everyone who installs it, ##
|
||||
## so the release must be on that server before its manifests are written. ##
|
||||
################################################################################
|
||||
if(-not $SkipDownloadCheck)
|
||||
{
|
||||
try
|
||||
{
|
||||
$null = Invoke-WebRequest -Uri $url -Method Head -MaximumRedirection 5 -TimeoutSec 60 -ErrorAction Stop
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw "$url does not answer ($($_.Exception.Message)). Publish the release and its assets on git.kof22.com first, then run this again."
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Scoop: a real JSON edit, so the file stays valid whatever its layout. ##
|
||||
@@ -96,7 +116,7 @@ Write-Progress-Line "Scoop manifest -> $Version"
|
||||
$nuspecPath = Join-Path $repoRoot 'packaging\chocolatey\gclo.nuspec'
|
||||
$nuspec = Get-Content -LiteralPath $nuspecPath -Raw
|
||||
$nuspec = $nuspec -replace '<version>[^<]+</version>', "<version>$Version</version>"
|
||||
$nuspec = $nuspec -replace '<releaseNotes>[^<]+</releaseNotes>', "<releaseNotes>https://github.com/KofTwentyTwo/gclo/releases/tag/v$Version</releaseNotes>"
|
||||
$nuspec = $nuspec -replace '<releaseNotes>[^<]+</releaseNotes>', "<releaseNotes>https://git.kof22.com/KofTwentyTwo/gclo/releases/tag/v$Version</releaseNotes>"
|
||||
Write-FileText -Path $nuspecPath -Text $nuspec
|
||||
|
||||
$installPath = Join-Path $repoRoot 'packaging\chocolatey\tools\chocolateyinstall.ps1'
|
||||
|
||||
@@ -46,7 +46,7 @@ scoop install gclo
|
||||
|
||||
**Chocolatey is not available yet.** The only version submitted, 1.0.1, is still in community moderation with the status "Waiting for Maintainer to take corrective action", so `choco install gclo` does not find it. 1.1.0 has not been submitted. `winget` support for the desktop app is not available either. Both are tracked in [#7](https://git.kof22.com/KofTwentyTwo/gclo/issues/7), and this section will be updated when a package is approved.
|
||||
|
||||
The channels available today (GitHub Releases, the in-app updater and Scoop) are all served over HTTPS.
|
||||
The channels available today (the project's Releases page, the in-app updater and Scoop) are all served over HTTPS from git.kof22.com.
|
||||
|
||||
## Verifying a release
|
||||
|
||||
@@ -120,7 +120,7 @@ On launch, gclo shows a brief branded splash, then the **Quick Sync** connect ca
|
||||
|
||||
**Share:** Help > Share gclo by email… opens a new message in your email program with an invitation already written: what gclo is, where to download the latest release, and how to install it. Type an address and send; gclo itself sends nothing. If no email program is set up, the text is shown and copied to the clipboard, to paste into a message by hand.
|
||||
|
||||
**Updates:** Help > Check for updates… downloads and applies the latest release for your channel. Self-update is only available in installed builds (not the portable zip or a local debug build).
|
||||
**Updates:** Help > Check for updates… downloads and applies the latest release for your channel from the project's releases on git.kof22.com. Versions up to 1.1.0 look for updates on GitHub; if they do not find 1.1.1 there, install it once by hand, and it updates itself from then on. Self-update is only available in installed builds (not the portable zip or a local debug build).
|
||||
|
||||
### Saved accounts
|
||||
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ is a vulnerability: please report it through the channel above.
|
||||
carries a [build-provenance attestation](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations),
|
||||
and ships with a CycloneDX SBOM. See "Verifying a release" in the
|
||||
[README](README.md#verifying-a-release).
|
||||
- Self-update downloads only from this repository's GitHub Releases and verifies each
|
||||
- Self-update downloads only from this project's releases on git.kof22.com and verifies each
|
||||
package against the release's `releases.<channel>.json` feed.
|
||||
- Setup.exe, the update stub and every gclo executable and library are
|
||||
**Authenticode-signed** with Azure Artifact Signing (SHA-256, RFC 3161 timestamp) in
|
||||
|
||||
+9
-5
@@ -1,24 +1,28 @@
|
||||
{
|
||||
"version": "1.1.0",
|
||||
"description": "Git Clone Large Organizations: clone or update every repository of a GitHub organization or user account, in parallel (command-line edition)",
|
||||
"homepage": "https://github.com/KofTwentyTwo/gclo",
|
||||
"homepage": "https://git.kof22.com/KofTwentyTwo/gclo",
|
||||
"license": "MIT",
|
||||
"notes": [
|
||||
"gclo needs a GitHub personal access token: set GITHUB_TOKEN, or pass --token-env, --token-file, or --token-stdin.",
|
||||
"The desktop app is a separate download: https://github.com/KofTwentyTwo/gclo/releases"
|
||||
"The desktop app is a separate download: https://git.kof22.com/KofTwentyTwo/gclo/releases"
|
||||
],
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/KofTwentyTwo/gclo/releases/download/v1.1.0/gclo-cli-win-x64-1.1.0.zip",
|
||||
"url": "https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v1.1.0/gclo-cli-win-x64-1.1.0.zip",
|
||||
"hash": "46f02d89c9d8a459e5b02598f9791fa7aab5c16b09c254bc997f8f235a6a49e2"
|
||||
}
|
||||
},
|
||||
"bin": "gclo.exe",
|
||||
"checkver": "github",
|
||||
"checkver": {
|
||||
"url": "https://git.kof22.com/api/v1/repos/KofTwentyTwo/gclo/releases/latest",
|
||||
"jsonpath": "$.tag_name",
|
||||
"regex": "v([\\d.]+)"
|
||||
},
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/KofTwentyTwo/gclo/releases/download/v$version/gclo-cli-win-x64-$version.zip",
|
||||
"url": "https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v$version/gclo-cli-win-x64-$version.zip",
|
||||
"hash": {
|
||||
"url": "$baseurl/SHA256SUMS"
|
||||
}
|
||||
|
||||
+43
-7
@@ -200,6 +200,35 @@ Both install the same asset, `gclo-cli-win-x64-<version>.zip`, by URL and SHA-25
|
||||
- To re-point the manifests by hand (for example after a missed release):
|
||||
`./.github/scripts/Update-PackageManifests.ps1 -Version 1.2.3 -CliZipPath <zip>`.
|
||||
|
||||
## The release on git.kof22.com
|
||||
|
||||
From 1.1.1 on, the app's updater, the Scoop manifest and the Chocolatey package
|
||||
read releases from `https://git.kof22.com/KofTwentyTwo/gclo`
|
||||
([ADR 0009](adr/0009-updates-and-downloads-from-gitea.md)). The release workflow
|
||||
still builds, signs and publishes on GitHub. Until a pipeline on Gitea exists, a
|
||||
release is therefore **not done** until it also exists on Gitea, exactly as it
|
||||
does on GitHub:
|
||||
|
||||
- **The same tag and the same assets**, byte for byte: Setup.exe, the portable
|
||||
zip, the CLI zip, the `.nupkg` packages (full and delta), `releases.<channel>.json`,
|
||||
`SHA256SUMS`, the SBOMs. The updater needs the feed file and the packages; the
|
||||
package managers need the CLI zip and `SHA256SUMS`.
|
||||
- **The same prerelease flag.** A beta copied without the flag becomes the latest
|
||||
release (Scoop would see it, and stable installs would be offered nothing newer
|
||||
because their feed file is not in it). A stable release copied with the flag is
|
||||
invisible to stable installs.
|
||||
- **Before the package manifests are written.** `Update-PackageManifests.ps1`
|
||||
refuses to write a manifest whose download does not answer, so the `packages`
|
||||
job fails until the copy is done; run it again afterwards.
|
||||
- **No more than nine prereleases newer than the latest stable release.** The
|
||||
updater reads the ten newest releases and picks by channel; a longer run of betas
|
||||
hides the stable release from stable installs. Delete old prereleases.
|
||||
- **From the network the users are on.** Check the release page and one download
|
||||
from outside the local network.
|
||||
|
||||
Releases on Gitea can be edited and their assets replaced; nothing there is
|
||||
immutable. Treat a published version as final anyway (see "A broken release").
|
||||
|
||||
## Cutting a release, start to finish
|
||||
|
||||
1. **Open the release checklist issue** from the KofTwentyTwo standards'
|
||||
@@ -255,9 +284,12 @@ Both install the same asset, `gclo-cli-win-x64-<version>.zip`, by URL and SHA-25
|
||||
above; each fails loudly when it cannot publish.
|
||||
|
||||
7. **Verify.** On the [releases page](https://git.kof22.com/KofTwentyTwo/gclo/releases)
|
||||
every asset listed above is attached and
|
||||
every asset listed above is attached, with the same hashes as in `SHA256SUMS`
|
||||
and the right prerelease flag (see "The release on git.kof22.com"). On GitHub,
|
||||
where the release was built,
|
||||
`gh attestation verify <asset> --repo KofTwentyTwo/gclo --signer-repo KofTwentyTwo/standards`
|
||||
passes. Merge the packaging PR. Check the winget PR on microsoft/winget-pkgs.
|
||||
passes. Install the previous version and let it update itself to this one.
|
||||
Merge the packaging PR. Check the winget PR on microsoft/winget-pkgs.
|
||||
Tick the "Ship" section of the checklist issue.
|
||||
|
||||
8. **If something failed**, where it failed decides the recovery:
|
||||
@@ -288,13 +320,17 @@ Both install the same asset, `gclo-cli-win-x64-<version>.zip`, by URL and SHA-25
|
||||
|
||||
### A broken release
|
||||
|
||||
Releases are immutable and a published version is never deleted or reused
|
||||
(K22-REL-07): installed apps, package managers, and attestations may already
|
||||
refer to it. Instead:
|
||||
A published version is never deleted or reused (K22-REL-07): installed apps,
|
||||
package managers, and attestations may already refer to it. (GitHub enforces
|
||||
that; Gitea does not, so it is a rule to keep, not one to rely on.) Instead:
|
||||
|
||||
1. Edit the broken release's notes to say what is wrong and which version fixes
|
||||
it (`gh release edit v1.2.3 --notes-file ...`), and mark it as a prerelease if
|
||||
stable users must stop receiving it (`gh release edit v1.2.3 --prerelease`).
|
||||
it, and mark it as a prerelease if stable users must stop receiving it. Do
|
||||
both **on git.kof22.com**, which is where installed apps from 1.1.1 on and the
|
||||
package managers look (edit the release there, or
|
||||
`tea releases edit --repo KofTwentyTwo/gclo --prerelease true v1.2.3`), and on
|
||||
GitHub for the versions up to 1.1.0 that still look there
|
||||
(`gh release edit v1.2.3 --notes-file ...`, `gh release edit v1.2.3 --prerelease`).
|
||||
2. Fix the cause on `main` through a normal pull request.
|
||||
3. Tag the **next** patch version and let the pipeline publish it.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-0001: Use Velopack for install and self-update from GitHub Releases
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Status:** Accepted; amended by [ADR 0009](0009-updates-and-downloads-from-gitea.md) (releases are read from git.kof22.com)
|
||||
- **Date:** 2026-07-05 (recorded 2026-10-09)
|
||||
- **Pull request:** #64
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-0003: Make this repository its own Scoop bucket and ship the Chocolatey package source
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Status:** Accepted; amended by [ADR 0009](0009-updates-and-downloads-from-gitea.md) (releases are read from git.kof22.com)
|
||||
- **Date:** 2026-10-09
|
||||
- **Pull request:** #59
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 0009. Updates and release downloads come from the project's Gitea server
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-11-10
|
||||
- Amends: [0001](0001-velopack-for-install-and-update.md) (the update feed), [0003](0003-repository-as-scoop-bucket.md) (where the bucket and its downloads are)
|
||||
|
||||
## Context
|
||||
|
||||
The project's home moved from GitHub to `git.kof22.com` (#165): source, issues, and
|
||||
the releases with their assets. GitHub keeps a read-only copy. ADR 0001 chose
|
||||
Velopack with GitHub Releases as the update feed, and the Scoop and Chocolatey
|
||||
manifests download from GitHub Releases. Left as they were, the app would look for
|
||||
updates in a place where new releases no longer appear, and the package managers
|
||||
would install an old version for ever.
|
||||
|
||||
## Decision
|
||||
|
||||
- The in-app updater reads the releases of `https://git.kof22.com/KofTwentyTwo/gclo`
|
||||
through Velopack's `GiteaSource`, anonymously. The repository is public; no token
|
||||
is built into the app.
|
||||
- The Scoop manifest (homepage, download URL, version check, auto-update) and the
|
||||
Chocolatey package (download URL, release notes link) name the same server. The
|
||||
script that regenerates them for a release writes those URLs.
|
||||
- A release is complete only when its assets, including `releases.<channel>.json`
|
||||
and `SHA256SUMS`, are attached to the release on `git.kof22.com`. Where the
|
||||
release is built is a separate matter (see Consequences).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Versions up to 1.1.0 look for updates on GitHub. They find a later version only
|
||||
if it is also published there; otherwise they are updated once by hand. From
|
||||
1.1.1 on the app looks on `git.kof22.com`.
|
||||
- The availability and the integrity of updates now rest on a self-hosted server
|
||||
instead of GitHub's release hosting. The updater verifies a package against the
|
||||
hash in the feed, and the feed comes from the same server; it checks no signature
|
||||
and no attestation (it never did). Whoever can publish a release on that server,
|
||||
or holds its TLS certificate or DNS, can therefore deliver an update that installs
|
||||
without a warning. Authenticode signing (ADR 0008) protects a download a person
|
||||
runs, not an update the app applies. The threat model's T1 says the same and is
|
||||
marked accepted, not mitigated.
|
||||
- The server must be reachable from wherever the app runs. An app outside the
|
||||
network that can reach `git.kof22.com` reports that it could not check for
|
||||
updates, and Scoop cannot add the bucket.
|
||||
- Velopack reads the ten newest releases of any kind and then picks by channel.
|
||||
More than nine prereleases published after the latest stable release would hide
|
||||
that release from stable installs. Keep fewer, or delete old prereleases.
|
||||
- Build-provenance attestations are a GitHub feature. While releases are still
|
||||
built by the GitHub workflow and copied over, they exist and are verified against
|
||||
GitHub as the README describes; a release built on Gitea needs another answer.
|
||||
- The release workflow itself (`.github/workflows/release.yml`) publishes to GitHub
|
||||
today. Until a pipeline on Gitea exists, publishing a release includes copying its
|
||||
assets to the Gitea release. That step is part of the release procedure, not of
|
||||
this decision.
|
||||
- Anonymous requests to the server are subject to its own rate limits; the updater
|
||||
asks once at startup and when the user asks.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep the update feed on GitHub.** Rejected: the owner's decision is that
|
||||
everything a user touches comes from the project's own server, and a feed on a
|
||||
read-only copy would silently stop offering updates the day the copy stops being
|
||||
fed.
|
||||
- **A plain web folder (`SimpleWebSource`).** Rejected: Gitea's release pages are
|
||||
already there, and a second place to publish to is a second place to forget.
|
||||
@@ -29,7 +29,7 @@ GitHub.
|
||||
| Local data directory | `%LOCALAPPDATA%\KofTwentyTwo\gclo` (or `GCLO_DATA_DIR`): settings, accounts, logs. Deliberately outside the Velopack install root `%LOCALAPPDATA%\gclo`, which Setup.exe deletes on reinstall (#91) | Trusted; same user |
|
||||
| Target folder | Where repositories are written; may already contain repositories | Partly untrusted (pre-existing content, `.git` config) |
|
||||
| WSL default distribution | Runs `git clone` for the "Clone in WSL" recovery | Semi-trusted (user-installed Linux environment) |
|
||||
| Update feed | GitHub Releases of this repository, `releases.<channel>.json` | Untrusted until verified (HTTPS, package hashes, attestations) |
|
||||
| Update feed | The releases of this project on `git.kof22.com`, `releases.<channel>.json` | Untrusted transport, trusted publisher: HTTPS, and package hashes taken from the same feed (see T1) |
|
||||
| Package managers | Scoop bucket (this repo), Chocolatey, winget | Untrusted transport; assets verified by hash |
|
||||
|
||||
## 3. Data flow and trust boundaries
|
||||
@@ -43,7 +43,7 @@ flowchart LR
|
||||
app -->|settings, accounts, logs| data[(Data directory)]
|
||||
app -->|working trees| target[(Target folder)]
|
||||
app -->|env + script over stdin| wsl[WSL distribution]
|
||||
app -- HTTPS --> feed[(GitHub Releases: update feed)]
|
||||
app -- HTTPS --> feed[(git.kof22.com releases: update feed)]
|
||||
subgraph machine [Trust boundary: user's machine]
|
||||
app
|
||||
cred
|
||||
@@ -83,7 +83,7 @@ flowchart LR
|
||||
|
||||
| # | Boundary / component | Category | Threat | Likelihood | Impact | Mitigation | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| T1 | Update feed | Tampering | Attacker serves a malicious update | Low | Critical | HTTPS to this repository's Releases only; Velopack hash verification; immutable releases; provenance and SBOM attestations; Scorecard; Authenticode pending (EX-0002) | Mitigated (signing pending) |
|
||||
| T1 | Update feed | Tampering | Attacker serves a malicious update | Low | Critical | HTTPS to the project's releases on `git.kof22.com` only (ADR 0009); Velopack verifies each package against the size and hash in the feed. The feed and the packages come from the same server, and the updater does not check an Authenticode signature or an attestation: whoever controls that server, the repository's release permission, or the server's TLS certificate can deliver a package that installs without a warning. The mitigations are therefore the server's own: who may publish a release there, how its certificate and DNS are held. Releases are not immutable on Gitea. | Accepted for 1.1.1; depends on the server's access control |
|
||||
| T2 | Package managers / downloads | Tampering | Modified installer or CLI zip on a mirror | Low | High | `SHA256SUMS`; Scoop and Chocolatey manifests pin the hash; "Verifying a release" instructions | Mitigated |
|
||||
| T3 | Git remotes | Tampering / EoP | Repository with Windows-invalid or traversal paths (`..`, reserved names, case collisions) writes outside or corrupts the target | Medium | High | Pre-checkout validation of every incoming path, which also refuses a `.git` segment in any spelling Windows opens as that folder; `RepositoryPathResolver` containment; recovery by rename/skip or clone in WSL; `core.longpaths`. The recovery materializes the tree itself, so every write and delete it makes goes through `WorkingTree`: inside the working directory, never in `.git`, never through a link, and an existing file is replaced rather than written through (#141). Residual: the link check and the write are two steps, so a link swapped in between them by something already running as the user is not caught | Mitigated |
|
||||
| T4 | Git remotes | Tampering | Hostile `.git` config or hooks in a pre-existing target repository | Low | High | gclo runs no hooks (libgit2, no shell); only fetch and fast-forward; `gclo.checkoutpending` marks incomplete trees; a folder is fetched only when its `origin` names the listed repository, and the remote contacted is the listed URL, not origin's spelling of it; a `url.<base>.insteadOf` rewrite in the folder's own configuration is refused, and so is its `core.sshCommand` when the fetch is not HTTPS (`RemoteUrl`, #139). The user's global git configuration still applies, as it does to git itself. The recovery file `.git\gclo-recovery.json` is validated when it is loaded: a listed path that is not in the working tree fails the row instead of being deleted (#141) | Mitigated for the remote, the transport program, and the recovery file; other `.git` content of an existing folder is still trusted |
|
||||
@@ -122,3 +122,4 @@ flowchart LR
|
||||
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | T15 added: text from repositories and servers is shown with control, separator and format characters made visible, in the activity log and in everything the CLI prints as text, including characters a legacy console code page would turn into control bytes; the desktop app's surfaces are named as not covered (#143) |
|
||||
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | The project's home moved to `git.kof22.com`: the Help menu, the About dialog, the CLI's error text and the documentation link there, and the Scoop bucket is added from there (the manifest that pins the download URL and its hash now comes from that server over HTTPS; T2 is unchanged in kind). Help > Share hands a fixed text (what gclo is, where to download it, the sender's gclo version) to the system's mail handler as a `mailto:` link with no recipient, or shows it and copies it to the clipboard; nothing about the user, their accounts, or their repositories is in it, and the address it is built from must be a plain http(s) host and path (#163, #165). The update feed and the release downloads named in package manifests are still GitHub Releases at this revision. |
|
||||
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | Working tree (#174; the T3 and T4 rows are unchanged, this adds to what they rely on). On Windows, a forced checkout into a folder whose tree is not finished (the first checkout of a clone that was empty, and the completion of a checkout that was cut off) is preceded by refusals that change nothing: a link on the way or at a destination (except, at a destination where the repository itself has a symbolic link, a link that leads where the repository's does), a file where a folder is needed (also on the way to a submodule), a folder that is not empty where a file is needed, a file at a destination that is not what the unfinished checkout wrote there, and anything but an empty folder where the repository has a submodule. "What the unfinished checkout wrote" is the content, or the beginning of it, of the commit the branch is on or of a commit recorded in `gclo.checkoutwrittenbefore`, as stored or as the checkout filters make it, carriage returns aside. An unfinished checkout is a marked folder that has no index, or that does not hold every file of its commit; a marked folder that has an index and every file is updated like any clone, without force. A completion is refused when the index names a file the commit does not have (somebody added it with git, and the forced checkout would remove it). An unfinished checkout is completed at the commit its branch is on, before the branch moves, except where the branch must move first (a tree Windows cannot write, or a history origin rewrote); the commit it leaves is then recorded, and a path recovery of that folder accepts a file that is, whole, what a recorded commit has. A repository that has local branches is not treated as an empty clone; one without follows origin's default branch only when its folder holds nothing but `.git`. The folder is marked unfinished before its branch is created. One git operation per folder at a time within a process (not across processes, and not across spellings of a path that only the file system can resolve). The WSL clone is made in `~/gclo/.gclo-tmp` and renamed into place when complete (`mv -T`, after a check that nothing is there). Not covered: a file created in the folder between the check and the checkout that follows it; a file of the user's at a destination that is empty, or holds the repository's content or the beginning of it, which is replaced; a link of the user's that leads exactly where the repository's symbolic link at that path does, which is replaced by the repository's; a marked folder whose index somebody's git wrote and whose last file was cut off, which is taken for finished (git shows that file as changed); files of a commit the branch left that the new commit does not have, which stay in the folder untracked; an unfinished clone in which a folder of an earlier commit became a file, or that was cut off by a power loss leaving files of zeros, which is refused until the folder is cleared; platforms other than Windows, where the checkout is forced as before. |
|
||||
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | Update feed and release downloads moved from GitHub Releases to the project's releases on `git.kof22.com` (ADR 0009, #170): the in-app updater reads the feed with Velopack's Gitea source, anonymously over HTTPS, and the Scoop and Chocolatey manifests download from there. The updater verifies packages against the hashes in the feed, as before; it has never checked a signature or an attestation. The trust that was placed in GitHub's release hosting (account security, immutable releases) is now placed in a self-hosted server: whoever can publish a release there, or holds its TLS certificate or DNS, can deliver an update that installs without a warning. T1 is rewritten to say so and its status changed from mitigated to accepted. The manifests still pin a SHA-256 (T2). |
|
||||
@@ -45,7 +45,7 @@ public sealed class PackagingManifestTests
|
||||
private static string CliZipUrlPattern(string version)
|
||||
{
|
||||
string v = Regex.Escape(version);
|
||||
return $@"^https://github\.com/KofTwentyTwo/gclo/releases/download/v{v}/gclo-cli-win-x64(-{v})?\.zip$";
|
||||
return $@"^https://git\.kof22\.com/KofTwentyTwo/gclo/releases/download/v{v}/gclo-cli-win-x64(-{v})?\.zip$";
|
||||
}
|
||||
|
||||
|
||||
@@ -57,10 +57,24 @@ public sealed class PackagingManifestTests
|
||||
JsonElement x64 = root.GetProperty("architecture").GetProperty("64bit");
|
||||
Assert.Equal("gclo.exe", root.GetProperty("bin").GetString());
|
||||
Assert.Equal("MIT", root.GetProperty("license").GetString());
|
||||
Assert.Equal("github", root.GetProperty("checkver").GetString());
|
||||
string autoupdateUrl = root.GetProperty("autoupdate").GetProperty("architecture").GetProperty("64bit").GetProperty("url").GetString()!;
|
||||
Assert.Contains("v$version/gclo-cli-win-x64-$version.zip", autoupdateUrl);
|
||||
Assert.EndsWith("/SHA256SUMS", root.GetProperty("autoupdate").GetProperty("architecture").GetProperty("64bit").GetProperty("hash").GetProperty("url").GetString());
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Everything is asked of the project's own server (ADR 0009): the newest //
|
||||
// version (the tag of its latest release, 'v1.2.3' read as '1.2.3'), the //
|
||||
// download of a later version, its hash, and the pages the notes name. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
JsonElement checkver = root.GetProperty("checkver");
|
||||
Assert.Equal("https://git.kof22.com/api/v1/repos/KofTwentyTwo/gclo/releases/latest", checkver.GetProperty("url").GetString());
|
||||
Assert.Equal("$.tag_name", checkver.GetProperty("jsonpath").GetString());
|
||||
Assert.Equal(@"v([\d.]+)", checkver.GetProperty("regex").GetString());
|
||||
Assert.Equal("https://git.kof22.com/KofTwentyTwo/gclo", root.GetProperty("homepage").GetString());
|
||||
JsonElement autoupdate = root.GetProperty("autoupdate").GetProperty("architecture").GetProperty("64bit");
|
||||
Assert.Equal(
|
||||
"https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v$version/gclo-cli-win-x64-$version.zip",
|
||||
autoupdate.GetProperty("url").GetString());
|
||||
Assert.Equal("$baseurl/SHA256SUMS", autoupdate.GetProperty("hash").GetProperty("url").GetString());
|
||||
Assert.All(
|
||||
root.GetProperty("notes").EnumerateArray().Select(note => note.GetString()!),
|
||||
note => Assert.DoesNotContain("github.com/KofTwentyTwo", note, StringComparison.Ordinal));
|
||||
return (root.GetProperty("version").GetString()!, x64.GetProperty("url").GetString()!, x64.GetProperty("hash").GetString()!);
|
||||
}
|
||||
|
||||
@@ -73,7 +87,9 @@ public sealed class PackagingManifestTests
|
||||
XElement metadata = nuspec.Root.Element(ns + "metadata")!;
|
||||
Assert.Equal("gclo", metadata.Element(ns + "id")!.Value);
|
||||
string version = metadata.Element(ns + "version")!.Value;
|
||||
Assert.EndsWith($"/releases/tag/v{version}", metadata.Element(ns + "releaseNotes")!.Value);
|
||||
Assert.Equal($"https://git.kof22.com/KofTwentyTwo/gclo/releases/tag/v{version}", metadata.Element(ns + "releaseNotes")!.Value);
|
||||
Assert.DoesNotContain("github.com/KofTwentyTwo", nuspec.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("githubusercontent.com", nuspec.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string install = File.ReadAllText(Path.Combine(RepoRoot, "packaging", "chocolatey", "tools", "chocolateyinstall.ps1"));
|
||||
string url = Regex.Match(install, @"url64bit\s*=\s*'(?<value>[^']+)'", RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(1)).Groups["value"].Value;
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace gclo.Services;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Self-update via Velopack, with releases hosted on the app's public GitHub repository.
|
||||
/// Self-update via Velopack, with releases hosted on the project's public repository (ADR 0009).
|
||||
/// Updates only work in Velopack-installed builds: under F5, loose unpackaged builds, or
|
||||
/// MSIX packages <see cref="IsSupported"/> is false. Every member is guarded — failures are
|
||||
/// reported as strings and must never crash the app over something as optional as an update.
|
||||
@@ -25,7 +25,12 @@ namespace gclo.Services;
|
||||
/// </summary>
|
||||
public sealed class UpdateService
|
||||
{
|
||||
private const string RepoUrl = "https://github.com/KofTwentyTwo/gclo";
|
||||
/// <summary>
|
||||
/// The repository whose releases are the update feed: the project's home on its own
|
||||
/// Gitea server (ADR 0009). Public, so the feed and the packages are read without a
|
||||
/// token; nothing but the request for them is sent there.
|
||||
/// </summary>
|
||||
private const string ReleasesUrl = "https://git.kof22.com/KofTwentyTwo/gclo";
|
||||
|
||||
private readonly IActivityLog _log;
|
||||
|
||||
@@ -80,7 +85,7 @@ public sealed class UpdateService
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks GitHub releases for a newer version and remembers it for
|
||||
/// Checks the project's releases for a newer version and remembers it for
|
||||
/// <see cref="DownloadAndApplyAsync"/>. Never throws: failures come back in
|
||||
/// <see cref="UpdateCheckResult.Error"/>.
|
||||
/// </summary>
|
||||
@@ -98,7 +103,7 @@ public sealed class UpdateService
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_log.Error($"Update check failed before contacting GitHub: {ex.Message}", ex);
|
||||
_log.Error($"Update check failed before contacting the server: {ex.Message}", ex);
|
||||
return new UpdateCheckResult(null, ex.Message);
|
||||
}
|
||||
|
||||
@@ -167,8 +172,8 @@ public sealed class UpdateService
|
||||
/// Created lazily so merely constructing the service can never fail; callers wrap this
|
||||
/// in try/catch and treat a throwing manager as "updates not supported".
|
||||
/// A prerelease install (semver has a '-suffix') runs on the dev channel, whose
|
||||
/// packages are attached to GitHub *prerelease* releases — GithubSource must be
|
||||
/// told to look at prereleases or dev installs can never find any update at all.
|
||||
/// packages are attached to *prerelease* releases — the source must be told to
|
||||
/// look at prereleases or dev installs can never find any update at all.
|
||||
/// Stable installs keep prereleases out of consideration; Velopack's channel
|
||||
/// filtering then matches packages to the installed channel either way.
|
||||
/// </summary>
|
||||
@@ -179,7 +184,7 @@ public sealed class UpdateService
|
||||
bool prerelease = BuildVersion
|
||||
.Describe(Assembly.GetExecutingAssembly())
|
||||
.Contains('-');
|
||||
_manager = new UpdateManager(new GithubSource(RepoUrl, null, prerelease));
|
||||
_manager = new UpdateManager(new GiteaSource(ReleasesUrl, null, prerelease));
|
||||
}
|
||||
return _manager;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<packageSourceUrl>https://git.kof22.com/KofTwentyTwo/gclo/src/branch/main/packaging/chocolatey</packageSourceUrl>
|
||||
<docsUrl>https://git.kof22.com/KofTwentyTwo/gclo/src/branch/main/docs/CLI.md</docsUrl>
|
||||
<bugTrackerUrl>https://git.kof22.com/KofTwentyTwo/gclo/issues</bugTrackerUrl>
|
||||
<licenseUrl>https://raw.githubusercontent.com/KofTwentyTwo/gclo/main/LICENSE</licenseUrl>
|
||||
<licenseUrl>https://git.kof22.com/KofTwentyTwo/gclo/raw/branch/main/LICENSE</licenseUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<iconUrl>https://raw.githubusercontent.com/KofTwentyTwo/gclo/main/gclo/Assets/Brand/gclo-icon.svg</iconUrl>
|
||||
<iconUrl>https://git.kof22.com/KofTwentyTwo/gclo/raw/branch/main/gclo/Assets/Brand/gclo-icon.svg</iconUrl>
|
||||
<tags>git github clone mirror organization cli</tags>
|
||||
<summary>Clone or update every repository of a GitHub organization in one pass (CLI).</summary>
|
||||
<description><![CDATA[
|
||||
@@ -34,9 +34,9 @@ This package installs the self-contained command-line edition (`gclo.exe`, no .N
|
||||
|
||||
A GitHub personal access token is required: set `GITHUB_TOKEN`, or use `--token-env`, `--token-file`, or `--token-stdin` (there is deliberately no option that takes the token on the command line). The full command reference is the package's documentation link (docs/CLI.md in the repository).
|
||||
|
||||
The desktop app (WinUI 3) is a separate, Authenticode-signed download from the GitHub Releases page.
|
||||
The desktop app (WinUI 3) is a separate, Authenticode-signed download from the project's Releases page.
|
||||
]]></description>
|
||||
<releaseNotes>https://github.com/KofTwentyTwo/gclo/releases/tag/v1.1.0</releaseNotes>
|
||||
<releaseNotes>https://git.kof22.com/KofTwentyTwo/gclo/releases/tag/v1.1.0</releaseNotes>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="tools\**" target="tools" />
|
||||
|
||||
@@ -11,7 +11,7 @@ $toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$packageArgs = @{}
|
||||
$packageArgs.packageName = 'gclo'
|
||||
$packageArgs.unzipLocation = $toolsDir
|
||||
$packageArgs.url64bit = 'https://github.com/KofTwentyTwo/gclo/releases/download/v1.1.0/gclo-cli-win-x64-1.1.0.zip'
|
||||
$packageArgs.url64bit = 'https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v1.1.0/gclo-cli-win-x64-1.1.0.zip'
|
||||
$packageArgs.checksum64 = '46f02d89c9d8a459e5b02598f9791fa7aab5c16b09c254bc997f8f235a6a49e2'
|
||||
$packageArgs.checksumType64 = 'sha256'
|
||||
|
||||
|
||||
Reference in new issue
Block a user