#!/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: 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"