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.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user