Manifests and a static winget source for jcollie's Windows programs, built and signed on Linux.
winget-pkgs CONTRIBUTING.md
10 kB · 210 lines
Markdown
at main

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, holding JCOllie.<Program>.yaml, JCOllie.<Program>.installer.yaml and JCOllie.<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, with normalize.py and version.py ported from winget-cli), packing the MSIX (msix.py), checking manifests (check.py, against winget-cli's manifest schemas in schemas/, 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-certificate is 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.