aitingtingya_mv-obcc/scripts/release-github-clean.sh
2026-07-06 21:38:05 +08:00

260 lines
7.8 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
SOURCE_ROOT="${SOURCE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
CLEAN_ROOT="${CLEAN_ROOT:-/Users/gingerman/obsidian/git-learn-md/mv-obcc_clean}"
REPO="aitingtingya/mv-obcc"
REPO_URL="https://github.com/${REPO}.git"
VERSION="${VERSION:-$(SOURCE_ROOT="$SOURCE_ROOT" node -p "require(process.env.SOURCE_ROOT + '/package.json').version")}"
SUCCESS=0
cleanup() {
if [[ "$SUCCESS" == "1" ]]; then
rm -rf "$CLEAN_ROOT"
else
echo "Release failed; keeping temporary repository for debugging: $CLEAN_ROOT" >&2
fi
}
trap cleanup EXIT
get_github_token() {
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
printf "%s" "$GITHUB_TOKEN"
return 0
fi
local credential token
credential="$(
printf "protocol=https\nhost=github.com\nusername=x-access-token\n\n" \
| GIT_TERMINAL_PROMPT=0 git credential fill 2>/dev/null || true
)"
token="$(printf "%s\n" "$credential" | sed -n "s/^password=//p" | head -n 1)"
if [[ -n "$token" ]]; then
printf "%s" "$token"
return 0
fi
cat >&2 <<'EOF'
GitHub token not found.
Provide it by either:
1. exporting GITHUB_TOKEN for this command, or
2. storing it once in git credential helper / macOS Keychain:
git config --global credential.helper osxkeychain
printf 'protocol=https\nhost=github.com\nusername=x-access-token\npassword=YOUR_TOKEN\n\n' | git credential-osxkeychain store
Do not write the real token into repository files.
EOF
return 1
}
GITHUB_TOKEN_RESOLVED="$(get_github_token)"
if [[ ! "$GITHUB_TOKEN_RESOLVED" =~ ^(ghp_|github_pat_) ]]; then
echo "Resolved GitHub token does not look like a GitHub PAT. Abort." >&2
exit 1
fi
BASIC_AUTH="$(printf "x-access-token:%s" "$GITHUB_TOKEN_RESOLVED" | base64 | tr -d "\n")"
GIT_AUTH_HEADER="Authorization: Basic ${BASIC_AUTH}"
echo "Preparing clean release ${VERSION} from ${SOURCE_ROOT}"
rm -rf "$CLEAN_ROOT"
git clone "$REPO_URL" "$CLEAN_ROOT"
find "$CLEAN_ROOT" -mindepth 1 -maxdepth 1 ! -name ".git" -exec rm -rf {} +
allowlist=(
"src"
"scripts"
".github"
".gitignore"
"main.ts"
"manifest.json"
"styles.css"
"esbuild.config.mjs"
"eslint.config.mjs"
"tsconfig.json"
"package.json"
"package-lock.json"
"README.md"
"DEVELOPMENT-GUIDELINES.md"
"LATEX-SUITE-UPGRADE.md"
"RELEASE-PROCESS.md"
"THIRD_PARTY_NOTICES.md"
"LICENSE"
"TEST-REPORT.md"
"WINDOWS-VALIDATION.md"
"versions.json"
)
for item in "${allowlist[@]}"; do
if [[ -e "$SOURCE_ROOT/$item" ]]; then
mkdir -p "$(dirname "$CLEAN_ROOT/$item")"
cp -R "$SOURCE_ROOT/$item" "$CLEAN_ROOT/$item"
fi
done
cd "$CLEAN_ROOT"
rm -rf tests vitest.config.ts node_modules dist release .obsidian .DS_Store
node <<'NODE'
const fs = require("node:fs");
const packagePath = "package.json";
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
pkg.scripts = pkg.scripts || {};
delete pkg.scripts.test;
delete pkg.scripts["test:watch"];
pkg.scripts.verify = "npm run lint && npm run typecheck && npm run build";
for (const dep of ["vitest", "jsdom", "@types/jsdom"]) {
delete pkg.devDependencies?.[dep];
}
fs.writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
const tsconfigPath = "tsconfig.json";
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"));
tsconfig.compilerOptions = tsconfig.compilerOptions || {};
if (Array.isArray(tsconfig.compilerOptions.types)) {
tsconfig.compilerOptions.types = tsconfig.compilerOptions.types.filter(
(type) => type !== "vitest/globals",
);
}
if (Array.isArray(tsconfig.include)) {
tsconfig.include = tsconfig.include.filter((entry) => entry !== "tests/**/*.ts");
}
fs.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
NODE
run_logged() {
local name="$1"
shift
echo "Running ${name}..."
"$@" > ".release-${name}.log" 2>&1 || {
tail -n 120 ".release-${name}.log" >&2
exit 1
}
}
run_logged npm-install npm install --package-lock-only
run_logged npm-ci npm ci
run_logged verify npm run verify
run_logged package npm run package
test ! -d tests
test ! -f vitest.config.ts
node <<'NODE'
const lock = JSON.parse(require("node:fs").readFileSync("package-lock.json", "utf8"));
for (const name of [
"node_modules/vite",
"node_modules/rolldown",
"node_modules/@napi-rs/wasm-runtime",
"node_modules/@emnapi/core",
"node_modules/@emnapi/runtime",
]) {
if (lock.packages?.[name]) throw new Error(`Unexpected test dependency in lockfile: ${name}`);
}
NODE
if grep -RInE 'ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]+|x-access-token:(ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]+)' \
--exclude-dir=.git \
--exclude="RELEASE-PROCESS.md" \
.; then
echo "Potential GitHub token found in release tree. Abort." >&2
exit 1
fi
stage_items=()
for item in \
.gitignore \
src \
scripts \
.github \
main.ts \
manifest.json \
styles.css \
esbuild.config.mjs \
eslint.config.mjs \
tsconfig.json \
package.json \
package-lock.json \
README.md \
DEVELOPMENT-GUIDELINES.md \
LATEX-SUITE-UPGRADE.md \
RELEASE-PROCESS.md \
THIRD_PARTY_NOTICES.md \
LICENSE \
TEST-REPORT.md \
WINDOWS-VALIDATION.md \
versions.json; do
[[ -e "$item" ]] && stage_items+=("$item")
done
git add -- "${stage_items[@]}"
if git diff --cached --name-only | grep -E '^(node_modules|dist|release|tests|\.obsidian|vitest\.config\.ts)'; then
echo "Forbidden development/test path staged. Abort." >&2
exit 1
fi
if git diff --cached --quiet; then
echo "No file changes to commit."
else
git commit -m "release: ${VERSION}"
fi
echo "Pushing main..."
git -c http.extraHeader="$GIT_AUTH_HEADER" push "$REPO_URL" main
echo "Publishing tag ${VERSION}..."
git tag -f "$VERSION"
git -c http.extraHeader="$GIT_AUTH_HEADER" push "$REPO_URL" "refs/tags/${VERSION}" --force
set +e
release_lookup="$(
curl -fsS \
-H "Authorization: Bearer $GITHUB_TOKEN_RESOLVED" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${REPO}/releases/tags/${VERSION}" 2>/dev/null
)"
lookup_status=$?
set -e
if [[ $lookup_status -eq 0 && -n "$release_lookup" ]]; then
release_id="$(node -e 'const r=JSON.parse(process.argv[1]); if (r.id) process.stdout.write(String(r.id));' "$release_lookup")"
if [[ -n "$release_id" ]]; then
echo "Deleting existing release ${VERSION}..."
curl -fsS -X DELETE \
-H "Authorization: Bearer $GITHUB_TOKEN_RESOLVED" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${REPO}/releases/${release_id}" >/dev/null
fi
fi
release_payload="$(node -e 'const version = process.argv[1]; process.stdout.write(JSON.stringify({ tag_name: version, target_commitish: "main", name: version, body: `mv-SenceAI ${version}`, draft: false, prerelease: false }));' "$VERSION")"
release_json="$(
curl -fsS -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN_RESOLVED" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d "$release_payload" \
"https://api.github.com/repos/${REPO}/releases"
)"
upload_url="$(node -e 'const r = JSON.parse(process.argv[1]); process.stdout.write(r.upload_url.replace(/\{.*\}$/, ""));' "$release_json")"
html_url="$(node -e 'const r = JSON.parse(process.argv[1]); process.stdout.write(r.html_url);' "$release_json")"
for asset in "dist/main.js:main.js" "manifest.json:manifest.json" "styles.css:styles.css"; do
src="${asset%%:*}"
name="${asset##*:}"
curl -fsS -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN_RESOLVED" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$src" \
"${upload_url}?name=$name" >/dev/null
echo "Uploaded ${name}"
done
unset GITHUB_TOKEN_RESOLVED BASIC_AUTH GIT_AUTH_HEADER
SUCCESS=1
echo "Published ${VERSION}: ${html_url}"