Files
max d45639bf2f Build, publish to Gitea releases, and install the three extensions
Answers two things: how to install these without a marketplace, and how to keep
settings in sync when VS Code's Settings Sync cannot be pointed at Gitea.

- build.ps1 packages each extension into dist/
- publish.ps1 creates a release and uploads them, replacing same-named assets so a
  rolling "latest" tag stays clean
- install.ps1 downloads a release's assets and installs them, or -Local from dist/
- settings.ps1 copies User/ between the repo and the machine, and installs the
  marketplace extensions recorded in extensions.txt
- dev-link.ps1 junctions the sources into ~/.vscode/extensions for development
- ci-release.sh plus a Gitea Actions workflow do the build and publish on a tag push

Settings are copied, not symlinked: an atomic save replaces a symlink with a regular
file and the sync stops without looking broken.

vsce/ pins vsce and undici so packaging works on Node 18, which current vsce does not
support.

test/run.ps1 drives the real scripts against a stub of the Gitea release API. It
caught the multipart Content-Disposition being unquoted on .NET Framework, and
Invoke-WebRequest dropping the Authorization header across a redirect.
2026-09-07 19:35:20 +02:00

143 lines
5.4 KiB
Bash

#!/usr/bin/env bash
# Packages every extension in config.json and uploads the .vsix files to a Gitea release.
#
# This is the CI half of publish.ps1: the same steps, in bash, for a Linux runner with no
# PowerShell. Driven by .gitea/workflows/release.yml, but it runs anywhere with bash, git,
# node and curl. JSON goes through node rather than jq, because node has to be there
# anyway to build the extensions and jq often is not.
#
# Environment:
# GITEA_SERVER base URL, e.g. https://gitea.example.com (CI: GITHUB_SERVER_URL)
# GITEA_OWNER owner of the release repo (CI: GITHUB_REPOSITORY_OWNER)
# GITEA_REPO name of the release repo
# GITEA_TOKEN token with write:repository
# TAG release tag, default "latest"
# SKIP_CLONE non-empty: fail instead of cloning a missing extension
# SKIP_PACKAGE non-empty: upload whatever is already in dist/, build nothing
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
config="$root/config.json"
dist="$root/dist"
tag="${TAG:-latest}"
for tool in git node curl; do
command -v "$tool" >/dev/null || { echo "missing $tool" >&2; exit 1; }
done
for name in GITEA_SERVER GITEA_OWNER GITEA_REPO GITEA_TOKEN; do
[ -n "${!name:-}" ] || { echo "missing $name" >&2; exit 1; }
done
server="${GITEA_SERVER%/}"
api="$server/api/v1/repos/$GITEA_OWNER/$GITEA_REPO"
auth=(-H "Authorization: token $GITEA_TOKEN")
# Evaluates a JavaScript expression against JSON on stdin, one line per array element.
# Unparseable input yields nothing, so a 404 body reads as "no release".
jget() {
node -e '
let raw = "";
process.stdin.on("data", chunk => { raw += chunk; }).on("end", () => {
let json;
try { json = JSON.parse(raw); } catch (error) { return; }
const value = Function("j", "return (" + process.argv[1] + ")")(json);
if (value === undefined || value === null) { return; }
if (Array.isArray(value)) { value.forEach(item => console.log(item)); }
else { console.log(value); }
});
' "$1"
}
mkdir -p "$dist"
# --- package ---------------------------------------------------------------------
if [ -n "${SKIP_PACKAGE:-}" ]; then
echo "SKIP_PACKAGE set, using what is already in dist/"
else
rm -f "$dist"/*.vsix
node -e '
const config = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
for (const extension of config.extensions) {
console.log([extension.id, extension.path, extension.repo].join("\t"));
}
' "$config" > "$dist/.extensions.tsv"
while IFS=$'\t' read -r id relative repo; do
[ -n "$id" ] || continue
path="$root/$relative"
echo "=== $id"
if [ ! -d "$path" ]; then
[ -z "${SKIP_CLONE:-}" ] || { echo " $path missing and SKIP_CLONE set" >&2; exit 1; }
# oauth2:<token> is how Gitea takes a token over https, so private repos clone.
authed="$(echo "$server" | sed -E "s#^(https?://)#\1oauth2:$GITEA_TOKEN@#")"
git clone --depth 1 "$authed/$GITEA_OWNER/$repo.git" "$path"
fi
pushd "$path" >/dev/null
if [ -f package-lock.json ]; then
npm ci --no-audit --no-fund
else
npm install --no-audit --no-fund
fi
name="$(node -p "require('./package.json').name")"
version="$(node -p "require('./package.json').version")"
npx --yes @vscode/vsce package \
--out "$dist/$name-$version.vsix" --allow-missing-repository
popd >/dev/null
done < "$dist/.extensions.tsv"
rm -f "$dist/.extensions.tsv"
fi
ls -l "$dist"/*.vsix
# --- release ---------------------------------------------------------------------
release="$(curl -sS "${auth[@]}" "$api/releases/tags/$tag" || true)"
id="$(printf '%s' "$release" | jget 'j.id')"
if [ -z "$id" ]; then
echo "creating release $tag"
body="$(TAG="$tag" node -e '
process.stdout.write(JSON.stringify({
tag_name: process.env.TAG,
name: "VS Code extensions (" + process.env.TAG + ")",
body: "Built by CI. Install with scripts/install.ps1.",
draft: false,
prerelease: false,
}));
')"
created="$(curl -sS -f -X POST "${auth[@]}" \
-H 'Content-Type: application/json' -d "$body" "$api/releases")"
id="$(printf '%s' "$created" | jget 'j.id')"
[ -n "$id" ] || { echo "could not create the release: $created" >&2; exit 1; }
release='{"assets":[]}'
fi
echo "release id $id"
# Uploads run from inside dist/ so curl gets a bare filename. An absolute path would
# do on Linux, but curl built for Windows cannot open an MSYS-style /d/... path, and
# a relative name is unambiguous everywhere.
pushd "$dist" >/dev/null
for base in *.vsix; do
# Replace rather than duplicate, so republishing the same tag stays clean.
old="$(printf '%s' "$release" | ASSET_NAME="$base" \
jget '(j.assets||[]).filter(a => a.name === process.env.ASSET_NAME).map(a => a.id)')"
for asset in $old; do
echo " replacing $base"
curl -sS -f -X DELETE "${auth[@]}" "$api/releases/$id/assets/$asset" >/dev/null
done
echo " uploading $base"
curl -sS -f -X POST "${auth[@]}" \
-F "attachment=@$base;type=application/octet-stream" \
"$api/releases/$id/assets?name=$base" >/dev/null
done
popd >/dev/null
echo "done: $server/$GITEA_OWNER/$GITEA_REPO/releases/tag/$tag"