Contributing #
This source carries only Jeff's own programs, under the single publisher
JCOllie, so a pull request adding someone else's package will be declined.
Fixes to the tooling, the documentation or an existing manifest are welcome at
https://tangled.org/jcollie.dev/winget-pkgs.
Setting up #
Everything the repository needs comes from its Nix flake:
nix develop
Layout #
manifests/j/JCOllie/<Program>/<Version>/— one directory per version of a package, holdingJCOllie.<Program>.yaml,JCOllie.<Program>.installer.yamlandJCOllie.<Program>.locale.en-US.yaml, exactly as in Microsoft's winget-pkgs.tools/wingetsource/— the Python program that builds the source: merging manifests (manifest.py), writing the index (index.py, withnormalize.pyandversion.pyported from winget-cli), packing the MSIX (msix.py), checking manifests (check.py, against winget-cli's manifest schemas inschemas/, copied unchanged), writing a package's first version (new_package.py), writing a new version from the newest (new_version.py), and reading an MSI's codes for both (msi.py).tests/— its tests.tests/data/winget-cli/is winget-cli's own name normalization test data, copied unchanged.certificate/JCOllie.cer— the public half of the signing certificate.tools/make-signing-certificateis how it was made; the private key is not in the repository.tools/setup-submitter— the wizard that sets up the submitter for a program; see below.spike/— scaffolding for proving the source on a Windows machine before it is published; see below.GLOSSARY.md— the words this project uses and the ones it avoids. Use its terms in issues, commit messages and code.docs/adr/— the decisions that shape the source and why they were made. Read the ones touching an area before changing it; a change that contradicts one should say so and argue for reopening it.
Building and testing #
nix develop -c python3 -m unittest discover -s tests -t .
nix develop -c python3 -m wingetsource build \
--manifests manifests --out _site \
--certificate certificate/JCOllie.cer \
--certificate-pem /path/to/signing.pem --key /path/to/signing.key
The build recreates _site/, the whole static site. The package version comes
from the commit time of HEAD; --time now overrides it, which is what a
second local build needs for winget to see it as newer.
The index writer follows winget-cli's Microsoft/Schema/1_0 to 1_7 code.
When changing it, check the result against Microsoft's own index: download
https://cdn.winget.microsoft.com/cache/source.msix, unzip Public/index.db
from it, and compare sqlite3 index.db .schema with ours.
Trying it on Windows before it is published #
spike/serve serves _site/ over HTTPS with caddy, because winget refuses a
source that is not https:// and reads source.msix with range requests. It
makes a throwaway TLS certificate for this host, which the Windows machine
fetches over plain HTTP from the second port and trusts first:
nix develop -c spike/serve # https on 9443, the TLS certificate on 9080
spike/manifests/ holds a pretend JCOllie.Clipwatch 0.1.0 that installs the
0.2.0 executable, so there is something to upgrade from. Build with only it,
install, then build again with --manifests manifests --manifests spike/manifests --time now and upgrade.
Manifests #
A version is added by a new directory, never by editing an existing one: a published manifest is what clients have already installed from. Removing a version is a pull request that deletes its directory.
A package's first version is written by new-package, from what is particular
to the program:
nix develop -c python3 -m wingetsource new-package --manifests manifests \
--identifier JCOllie.Zfj --version 0.4.0 --repository jeff/zfj --type msi \
--installer x64=https://git.jcollie.dev/jeff/zfj/releases/download/v0.4.0/zfj-0.4.0-x86_64-windows.msi \
--name zfj \
--short-description 'A command-line tool for Forgejo, built on Zig bindings for its whole API.' \
--description-file description.txt \
--license MIT --tag cli --tag forgejo --tag git --tag zig \
--release-notes-url https://git.jcollie.dev/jeff/zfj/releases/tag/v0.4.0
--type is portable, for an executable winget puts on the PATH, or msi.
--description gives the description inline, a newline between paragraphs,
and --description-file reads it from a file instead. --moniker and
--command default to the program's name in lower case, --release-date to
today in UTC, and --json prints {"directory": ...}. The publisher, the
URLs into the program's repository (the license's being
LICENSES/<SPDX>.txt on main), the SPDX and schema lines, the en-US
locale and the manifest version all follow from those. For an MSI, the
ProductCode, the UpgradeCode and whether it installs for all users
(ALLUSERS=1, written as Scope: machine) are read from the downloaded MSI
with msitools' msiinfo. Anything the program needs beyond that — a comment
on how its MSI is built, say — is added by hand in the same pull request.
It writes nothing if the package already has a version, if the identifier is
not JCOllie.<Program>, if an installer does not download from
git.jcollie.dev, if an MSI lacks either code, if several MSIs disagree on
their UpgradeCode or on installing for all users, or if what it would write
fails check's rules. Only a person runs it, so unlike new-version's, its
options may change.
Every later version is written from the newest, by winget's version ordering:
nix develop -c python3 -m wingetsource new-version --manifests manifests \
--identifier JCOllie.Clipwatch --version 0.3.0 \
--installer x64=https://git.jcollie.dev/jeff/clipwatch/releases/download/v0.3.0/clipwatch-x86_64-windows.exe \
--release-notes-url https://git.jcollie.dev/jeff/clipwatch/releases/tag/v0.3.0
It copies every file of the newest version, comments included, and replaces
only what changes with a release: PackageVersion, ReleaseDate
(--release-date, today in UTC by default), and each installer's
InstallerUrl and InstallerSha256, matched by Architecture, with the hash
taken by downloading it. The default locale's ReleaseNotesUrl becomes
--release-notes-url, or is dropped without it, and ReleaseNotes is always
dropped. It writes nothing if the version already exists, if the package has
no version yet, or if --installer does not name exactly the architectures
the newest version has; changing those is done by hand. With --json it
prints {"directory": ..., "copied_from": ...} rather than the new
directory alone.
An MSI's ProductCode changes with every release, so it is never copied: an
installer that has one is given the new MSI's, read from it with msitools'
msiinfo. --product-code x64={GUID} gives the code instead, and the MSI is
then not read. It writes nothing if the MSI cannot be read or has no
ProductCode, or if its UpgradeCode is not one the manifest already gives,
since winget would then not see it as the same program. It refuses for the same
reason when the installer file has a DisplayVersion, which it has no way to
be given, or one ProductCode at the top for every installer rather than one
under each.
This is what jeff/winget-submit
runs when a program submits its own release. Every program pins that action,
but the action runs whatever new-version is on main, so its options may be
added to and never changed or removed (ADR 0004).
Check a change before opening the pull request:
nix develop -c python3 -m wingetsource check --manifests manifests --changed-since origin/main
Every version is checked against the 1.12.0 manifest schemas, the way winget
validates them, and against the rules here: the identifier is
JCOllie.<Program>, the directory and file names follow from it and the
version, and the files agree. The installers of the versions changed since
origin/main must download from git.jcollie.dev and hash to the SHA-256 their
manifest gives, so those are downloaded; without --changed-since every
version's installers are. CI runs the same check on every pull request, and
on main once it merges; a branch is checked through its pull request rather
than on each push, so that a change is not tested twice.
main only moves through a merged pull request that has passed validation.
Branch protection enforces it for everyone, the repository's owner included:
nothing can be pushed to main directly, a pull request can merge only once
its test / test (pull_request) check has passed, and only jeff can merge
one. Every merge to main then rebuilds and publishes the source.
The submitter #
A program submits its own releases through
jeff/winget-submit, acting as
the submitter winget-bot: a restricted Forgejo user with write access to
this repository and nothing else, which can push branches and open pull
requests here but not merge them. Each program holds its own token for it, in
the Actions secret WINGET_BOT_TOKEN, limited to this repository and to the
write:repository and write:issue scopes, which are all Forgejo allows a
token limited to repositories.
tools/setup-submitter sets all of that up for one program, and is run again
for each new one. It creates winget-bot if it does not exist, makes it a
collaborator here, restricts merging to main to jeff, makes the program's
token, stores it as the program's secret, and checks what the token can and
cannot do:
nix develop -c tools/setup-submitter jeff/clipwatch
It needs fj logged in to git.jcollie.dev as an administrator. The bot's
password is random and kept only while the wizard runs; running it again
resets it, and replaces the program's token.
Licensing #
Every file carries an SPDX header or is covered by REUSE.toml, and
nix develop -c reuse lint must pass.