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.
143 lines
5.4 KiB
Bash
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"
|