Files
vs-code-setup/README.md
T

181 lines
8.7 KiB
Markdown

# VS Code setup
Builds three local VS Code extensions, publishes them to a **Gitea release**, installs
them from there, and keeps user settings in git.
| Extension | What it does |
| --- | --- |
| `colored-references` | Find All References in a syntax-highlighted virtual document, plus a sortable results panel |
| `vertical-tabs` | A vertical list of open tabs, colour-coded by project, with pinning |
| `dotnet-solution-launcher` | Startup project and solution configuration (`Test \| x64`) in the status bar; build and debug that pass the platform DotRush drops |
## The short version
```powershell
# once per machine, on the machine that builds
$env:GITEA_TOKEN = '<token>'
.\scripts\publish.ps1 -Build # package all three, upload to the "latest" release
# on any machine, including a fresh one
.\scripts\install.ps1 # download from the release and install
.\scripts\settings.ps1 -Pull # apply settings, keybindings, marketplace extensions
```
Set the server first — either in [`config.json`](config.json) or with `GITEA_URL`,
`GITEA_OWNER` and `GITEA_REPO`. The repo named there is where releases go; it does not
have to be this repo.
## Why not Settings Sync
**VS Code's built-in Settings Sync cannot be pointed at Gitea.** It signs in with a
Microsoft or GitHub account and talks to Microsoft's own sync service; there is no
setting for a different backend, self-hosted or otherwise. The old
Gist-based *Settings Sync* extension is out too, because Gitea has no gists.
So settings live in [`User/`](User/) as ordinary files, and
[`scripts/settings.ps1`](scripts/settings.ps1) copies them in either direction. That is
less magic than Settings Sync and more legible: a diff shows what changed.
It **copies rather than symlinks** on purpose. A symlink at
`%APPDATA%\Code\User\settings.json` is fragile — an editor that saves atomically (write
a temp file, rename it over the target) replaces the link with a regular file, and the
sync stops without anything looking broken.
Extensions from the marketplace are handled separately: `settings.ps1 -Push` records
them in `User/extensions.txt`, `-Pull` installs the missing ones. Ids starting `local.`
are skipped, because those are the three above and they come from the release.
## Scripts
| Script | Does |
| --- | --- |
| [`build.ps1`](scripts/build.ps1) | Packages each extension into `dist/*.vsix`. `-Clone` fetches missing sources from Gitea, `-Pull` updates them, `-Only <id>` narrows it |
| [`publish.ps1`](scripts/publish.ps1) | Creates the release if needed and uploads `dist/*.vsix`. `-Build` builds first, `-Tag` picks the tag |
| [`install.ps1`](scripts/install.ps1) | Downloads a release's assets and installs them. `-Local` installs from `dist/` instead, `-Uninstall` removes them |
| [`settings.ps1`](scripts/settings.ps1) | `-Push` machine to repo, `-Pull` repo to machine, `-Diff` compares |
| [`dev-link.ps1`](scripts/dev-link.ps1) | Junctions the sources into `~/.vscode/extensions` for development. `-Unlink` undoes it |
| [`ci-release.sh`](scripts/ci-release.sh) | The bash equivalent of `publish.ps1`, for the Gitea Actions runner. `SKIP_PACKAGE=1` uploads `dist/` without rebuilding |
Every script takes `-?` for its full help.
### Tags
`publish.ps1` defaults to a rolling **`latest`** release: publishing again replaces the
assets in place rather than adding duplicates, so `install.ps1` with no arguments always
gets current builds. Use `-Tag v0.2.0` for a snapshot you want to keep, and
`install.ps1 -Tag v0.2.0` to go back to it.
The tag itself is created on the default branch the first time. For a release that only
carries binaries the commit it points at does not mean much, but it is why `latest`
keeps pointing at an old commit — the assets are what move.
## Tokens
Never in this repo. `publish.ps1` and `install.ps1` read, in order:
1. `-Token <value>`
2. `$env:GITEA_TOKEN`
3. `%USERPROFILE%\.gitea-token`
Create one at `<gitea>/user/settings/applications` with **`write:repository`** scope.
`install.ps1` only reads, so a `read:repository` token is enough there — and if the
release repo is public it needs no token at all for the download, though the API call
that finds the release still wants one on most instances.
## Building on Node 18
`vsce` needs **Node 20 or newer**. On Node 18 it fails with
`ReferenceError: File is not defined`, from a transitive `undici`, and pinning an older
`vsce` does not help because `undici` still resolves to a current version.
[`vsce/package.json`](vsce/package.json) works around it by pinning both, and
`build.ps1` installs into that folder on first run. If you upgrade Node to 20+ you can
delete `vsce/` and use `npx @vscode/vsce package` directly.
Upgrading Node is the better fix. The workaround exists so a machine stuck on 18 is not
blocked.
## Automating it
[`.gitea/workflows/release.yml`](.gitea/workflows/release.yml) builds and publishes on a
`v*` tag push, or on demand. It needs:
- a registered `act_runner` on the instance
- a `GITEA_TOKEN` secret with `write:repository`, and read access to the extension repos
- the `actions/checkout` and `actions/setup-node` actions reachable — Gitea pulls those
from github.com unless the instance sets `[actions] DEFAULT_ACTIONS_URL`
The runner uses Node 20, so CI sidesteps the problem above entirely.
`ci-release.sh` needs only bash, git, node and curl — JSON goes through `node`, not `jq`,
since node is required anyway and `jq` frequently is not.
## Why not `tea`
Gitea's CLI ([`tea`](https://gitea.com/gitea/tea)) can do this — `tea release create`
and `tea release assets create` cover the upload. It is not used here because these
scripts then need nothing but PowerShell and `code`: no Go binary to install per
machine, no `tea login add` step, no second place a token lives. Downloading assets on
the install side is also plainer over the API than through `tea`.
If you already have `tea` set up, `tea release create --tag latest --asset dist/*.vsix`
is a fine substitute for `publish.ps1`.
## Layout
```
config.json Gitea server + the extensions and where they live
User/ settings.json, keybindings.json, snippets/, extensions.txt
dist/ built .vsix files (ignored)
scripts/ the scripts above
vsce/ pinned vsce, for Node 18 (node_modules ignored)
.gitea/workflows/ the release workflow
```
`config.json` paths are relative to this repo, so it expects the extension folders as
siblings. Move them and it is one edit.
## Tests
```powershell
.\test\run.ps1
```
Runs the real scripts against [`test/fake-gitea.js`](test/fake-gitea.js), a stub of the
release endpoints, with a stub `code` on `PATH` so nothing is installed into the editor.
It checks the things that fail quietly:
- the `.vsix` survives the upload **byte for byte** — it is a zip, so any text-encoding
step in the multipart body would corrupt it and still look like a success
- the token reaches the API, and survives the redirect on the download
- republishing a tag replaces its assets instead of accumulating duplicates
- every install passes `--force`
- `build.ps1` and `install.ps1 -Local` work with no Gitea configured at all
- `ci-release.sh` publishes the same set as `publish.ps1`
Two bugs came out of writing it. `MultipartFormDataContent.Add(content, name, fileName)`
emits an unquoted `name=attachment` on .NET Framework, which Gitea's Go parser accepts
but nothing guarantees — the header is now set explicitly. And `Invoke-WebRequest` drops
the `Authorization` header when it follows a redirect, so downloading a private repo's
asset returned 401; `Save-GiteaAsset` follows redirects itself, re-sending the token only
while the host does not change.
What it does not cover: a real Gitea instance, and whether VS Code loads the packages
once installed. Both were checked by hand.
## Known limitations
- **No auto-update.** VS Code only offers updates for marketplace extensions, so a new
release does not notify you. Re-run `install.ps1`.
- **`--force` on every install**, because VS Code otherwise declines to reinstall a
version it already has, which would silently do nothing whenever you republish
without bumping the version.
- **A private extension gallery** — pointing VS Code's `product.json` at your own
gallery so updates work normally — is possible but gets overwritten by VS Code
updates. Not worth it for three extensions.
- **`dev-link.ps1` and `install.ps1` conflict.** A junctioned source and an installed
`.vsix` are two copies of the same extension id; VS Code loads one arbitrarily.
`dev-link.ps1 -Unlink` before installing.
- **Windows-only paths.** The scripts assume `%APPDATA%\Code\User` and
`%USERPROFILE%\.vscode\extensions`. `ci-release.sh` is the portable half.