From 1ab9b5bdc29ba7d1c23870c9c4833130e1868728 Mon Sep 17 00:00:00 2001 From: freelw <“freelw81@qq.com“> Date: Wed, 12 Aug 2026 16:12:27 +0800 Subject: [PATCH] release: align signed builds and COS distribution --- .github/workflows/ci.yml | 43 +++++-- .github/workflows/release.yml | 81 +++++++++---- .gitignore | 1 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 18 ++- scripts/package-skill.sh | 34 ++++-- scripts/sign_and_notarize_macos.sh | 108 ++++++++++++++++++ scripts/upload-release-to-cos.sh | 58 ++++++++++ skills/lexmount-webfetch/SKILL.md | 9 +- .../lexmount-webfetch/scripts/bootstrap.ps1 | 11 +- skills/lexmount-webfetch/scripts/bootstrap.sh | 5 +- skills/lexmount-webfetch/scripts/doctor.ps1 | 11 +- skills/lexmount-webfetch/scripts/doctor.sh | 8 +- 14 files changed, 327 insertions(+), 64 deletions(-) create mode 100755 scripts/sign_and_notarize_macos.sh create mode 100755 scripts/upload-release-to-cos.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c2c4ea..8e319b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,20 +18,45 @@ jobs: - run: cargo clippy --all-targets --locked -- -D warnings - name: Verify release target contract run: | - test "$(grep -c '^ target:' .github/workflows/release.yml)" -eq 2 ! grep -q 'x86_64-unknown-linux' .github/workflows/release.yml ! grep -q 'x86_64-apple-darwin' .github/workflows/release.yml grep -q 'aarch64-apple-darwin' .github/workflows/release.yml grep -q 'x86_64-pc-windows-msvc' .github/workflows/release.yml + grep -q 'environment: macos-release' .github/workflows/release.yml + grep -q './scripts/sign_and_notarize_macos.sh' .github/workflows/release.yml + grep -q 'environment: cos-release' .github/workflows/release.yml + grep -q './scripts/upload-release-to-cos.sh cos-upload webfetch-cli' .github/workflows/release.yml + grep -q 'needs: \[build-macos, build-windows\]' .github/workflows/release.yml + ! grep -q 'skills/lexmount-webfetch/bin/' .github/workflows/release.yml + - run: bash -n scripts/sign_and_notarize_macos.sh scripts/upload-release-to-cos.sh - run: sh -n scripts/package-skill.sh skills/lexmount-webfetch/scripts/bootstrap.sh skills/lexmount-webfetch/scripts/doctor.sh + - name: Verify Skill platform selection and PATH isolation + run: | + test_dir="$(mktemp -d)" + trap 'rm -rf "$test_dir"' EXIT + mkdir -p "$test_dir/skill/scripts" "$test_dir/fake-path" + cp skills/lexmount-webfetch/scripts/doctor.sh "$test_dir/skill/scripts/doctor.sh" + ln -s /usr/bin/true "$test_dir/fake-path/webfetch-cli" + set +e + output="$(PATH="$test_dir/fake-path:/usr/bin:/bin" "$test_dir/skill/scripts/doctor.sh")" + status=$? + set -e + test "$status" -eq 2 + test "$output" = '{"ok":false,"error":"unsupported_platform","message":"This Skill supports macOS arm64 through scripts/doctor.sh and Windows x64 through scripts/doctor.ps1."}' + ! grep -q 'command -v webfetch-cli' skills/lexmount-webfetch/scripts/doctor.sh + ! grep -q 'Get-Command webfetch-cli' skills/lexmount-webfetch/scripts/doctor.ps1 + - name: Verify runtime packaging contract + run: | + ! grep -R -E 'python3[[:space:]]|python[[:space:]]+-m|uv[[:space:]]' scripts skills .github/workflows + - name: Stage excluded Skill package fixtures + run: | + mkdir -p skills/lexmount-webfetch/bin + printf 'macOS arm64 package fixture' > skills/lexmount-webfetch/bin/webfetch-cli + printf 'Windows x64 package fixture' > skills/lexmount-webfetch/bin/webfetch-cli.exe - run: ./scripts/package-skill.sh - - run: python3 -m zipfile --test dist/lexmount-webfetch.zip + - run: unzip -t dist/lexmount-webfetch.zip - name: Verify SkillHub ZIP root run: | - python3 - <<'PY' - import zipfile - with zipfile.ZipFile("dist/lexmount-webfetch.zip") as archive: - names = set(archive.namelist()) - assert "SKILL.md" in names - assert not any(name.startswith("lexmount-webfetch/") for name in names) - PY + unzip -Z1 dist/lexmount-webfetch.zip | grep -qx 'SKILL.md' + ! unzip -Z1 dist/lexmount-webfetch.zip | grep -q '^lexmount-webfetch/' + ! unzip -Z1 dist/lexmount-webfetch.zip | grep -q '^bin/' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49ec8a4..1706547 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,42 +8,60 @@ permissions: contents: write jobs: - build: - strategy: - fail-fast: false - matrix: - include: - - os: macos-14 - target: aarch64-apple-darwin - binary: webfetch-cli - suffix: "" - - os: windows-latest - target: x86_64-pc-windows-msvc - binary: webfetch-cli.exe - suffix: .exe - runs-on: ${{ matrix.os }} + build-macos: + runs-on: macos-14 + environment: macos-release steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: - targets: ${{ matrix.target }} + targets: aarch64-apple-darwin - run: cargo test --locked - - run: cargo build --release --locked --target ${{ matrix.target }} + - run: cargo build --release --locked --target aarch64-apple-darwin + - name: Sign and notarize + env: + MACOS_DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_P12_BASE64 }} + MACOS_DEVELOPER_ID_P12_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_P12_PASSWORD }} + APPLE_NOTARY_APPLE_ID: ${{ secrets.APPLE_NOTARY_APPLE_ID }} + APPLE_NOTARY_TEAM_ID: ${{ secrets.APPLE_NOTARY_TEAM_ID }} + APPLE_NOTARY_APP_PASSWORD: ${{ secrets.APPLE_NOTARY_APP_PASSWORD }} + run: ./scripts/sign_and_notarize_macos.sh target/aarch64-apple-darwin/release/webfetch-cli + - name: Package + run: | + version="${GITHUB_REF_NAME#v}" + asset="webfetch-cli-v${version}-aarch64-apple-darwin" + cp target/aarch64-apple-darwin/release/webfetch-cli "$asset" + shasum -a 256 "$asset" > "$asset.sha256" + - uses: actions/upload-artifact@v4 + with: + name: release-aarch64-apple-darwin + path: webfetch-cli-v*-aarch64-apple-darwin* + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + - run: cargo test --locked + - run: cargo build --release --locked --target x86_64-pc-windows-msvc - name: Package shell: bash run: | version="${GITHUB_REF_NAME#v}" - asset="webfetch-cli-v${version}-${{ matrix.target }}${{ matrix.suffix }}" - cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "$asset" - if command -v sha256sum >/dev/null; then sha256sum "$asset" > "$asset.sha256"; else shasum -a 256 "$asset" > "$asset.sha256"; fi + asset="webfetch-cli-v${version}-x86_64-pc-windows-msvc.exe" + cp target/x86_64-pc-windows-msvc/release/webfetch-cli.exe "$asset" + sha256sum "$asset" > "$asset.sha256" - uses: actions/upload-artifact@v4 with: - name: release-${{ matrix.target }} - path: webfetch-cli-v*-${{ matrix.target }}* + name: release-x86_64-pc-windows-msvc + path: webfetch-cli-v*-x86_64-pc-windows-msvc* publish: - needs: build + needs: [build-macos, build-windows] runs-on: ubuntu-latest + environment: cos-release steps: - uses: actions/checkout@v5 - uses: actions/download-artifact@v4 @@ -52,11 +70,28 @@ jobs: merge-multiple: true - name: Package SkillHub upload ZIP run: | - ./scripts/package-skill.sh version="${GITHUB_REF_NAME#v}" + ./scripts/package-skill.sh cp dist/lexmount-webfetch.zip "lexmount-webfetch-v${version}-skillhub.zip" sha256sum "lexmount-webfetch-v${version}-skillhub.zip" > "lexmount-webfetch-v${version}-skillhub.zip.sha256" - run: cat *.sha256 | sort -k2 > SHA256SUMS + - name: Upload platform binaries to Tencent Cloud COS + env: + TENCENT_CLOUD_SECRET_ID: ${{ secrets.TENCENT_CLOUD_SECRET_ID }} + TENCENT_CLOUD_SECRET_KEY: ${{ secrets.TENCENT_CLOUD_SECRET_KEY }} + COS_BUCKET: ${{ vars.COS_BUCKET }} + COS_REGION: ${{ vars.COS_REGION }} + COS_PUBLIC_BASE_URL: ${{ vars.COS_PUBLIC_BASE_URL }} + COS_OBJECT_PREFIX: ${{ vars.COS_OBJECT_PREFIX }} + run: | + version="${GITHUB_REF_NAME#v}" + mkdir cos-upload + cp \ + "webfetch-cli-v${version}-aarch64-apple-darwin" \ + "webfetch-cli-v${version}-x86_64-pc-windows-msvc.exe" \ + cos-upload/ + (cd cos-upload && sha256sum webfetch-cli-v* | sort -k2 > SHA256SUMS) + ./scripts/upload-release-to-cos.sh cos-upload webfetch-cli "${version}" - run: rm -f -- *.sha256 - uses: softprops/action-gh-release@v2 with: diff --git a/.gitignore b/.gitignore index 68e15b1..30c68bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target/ /dist/ +/skills/lexmount-webfetch/bin/ *.zip diff --git a/Cargo.lock b/Cargo.lock index 631bd59..970aa90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,7 +1202,7 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "lexmount-webfetch" -version = "0.1.0" +version = "0.1.1" dependencies = [ "base64 0.22.1", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5931855..b2ac83c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lexmount-webfetch" -version = "0.1.0" +version = "0.1.1" edition = "2024" license = "MIT" description = "Native Rust SDK and CLI for Lexmount WebFetch" diff --git a/README.md b/README.md index b013c37..53e65a5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Lexmount WebFetch CLI (Rust) -Native Rust SDK and command-line client for Lexmount WebFetch. It mirrors the -agent-facing Python `webfetch-cli` contract without requiring Python, `uv`, or -Git at runtime. +Native Rust SDK and command-line client for Lexmount WebFetch. ## Build @@ -36,6 +34,16 @@ direct-upload SkillHub ZIP with: ./scripts/package-skill.sh ``` -Tagged releases publish `lexmount-webfetch-v-skillhub.zip`, -`SHA256SUMS`, and exactly two raw binaries: macOS ARM64 and Windows x64. Linux +The Skill ZIP contains `SKILL.md`, references, and platform bootstrap scripts; +native executables are published separately. On first use, the matching script +downloads the pinned release from Tencent Cloud COS and verifies its SHA-256 +digest. Tagged releases publish the Skill ZIP, `SHA256SUMS`, and exactly two +standalone binaries: signed and notarized macOS ARM64 plus Windows x64. Linux and macOS Intel are not release platforms. + +The macOS signing job reads its certificate and notarization credentials from +the `macos-release` GitHub environment. The publish job uploads both platform +binaries to Tencent Cloud COS through the `cos-release` environment, using +`TENCENT_CLOUD_SECRET_ID` and `TENCENT_CLOUD_SECRET_KEY` secrets plus +`COS_BUCKET`, `COS_REGION`, `COS_PUBLIC_BASE_URL`, and `COS_OBJECT_PREFIX` +variables. diff --git a/scripts/package-skill.sh b/scripts/package-skill.sh index 1933396..7681f86 100755 --- a/scripts/package-skill.sh +++ b/scripts/package-skill.sh @@ -3,17 +3,29 @@ set -eu repo_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" skill_dir="$repo_dir/skills/lexmount-webfetch" dist_dir="$repo_dir/dist" +staging_dir="$(mktemp -d)" +trap 'rm -rf "$staging_dir"' EXIT INT TERM mkdir -p "$dist_dir" rm -f "$dist_dir/lexmount-webfetch.zip" -python3 - "$skill_dir" "$dist_dir/lexmount-webfetch.zip" <<'PY' -import pathlib, sys, zipfile -root = pathlib.Path(sys.argv[1]) -output = pathlib.Path(sys.argv[2]) -with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: - for path in sorted(p for p in root.rglob("*") if p.is_file()): - info = zipfile.ZipInfo(path.relative_to(root).as_posix(), (1980, 1, 1, 0, 0, 0)) - info.compress_type = zipfile.ZIP_DEFLATED - info.external_attr = (0o755 if path.suffix in {".sh", ".ps1"} else 0o644) << 16 - archive.writestr(info, path.read_bytes()) -PY + +( + cd "$skill_dir" + find . -type f ! -name '.DS_Store' ! -path './bin/*' \ + -print | LC_ALL=C sort | + while IFS= read -r relative_path; do + mkdir -p "$staging_dir/$(dirname -- "$relative_path")" + cp "$relative_path" "$staging_dir/$relative_path" + done +) + +find "$staging_dir" -type d -exec chmod 0755 {} + +find "$staging_dir" -type f -exec chmod 0644 {} + +find "$staging_dir/scripts" -type f \( -name '*.sh' -o -name '*.ps1' \) -exec chmod 0755 {} + +find "$staging_dir" -exec touch -t 198001010000 {} + + +( + cd "$staging_dir" + find . -type f -print | LC_ALL=C sort | sed 's|^\./||' | + zip -X -9 -q "$dist_dir/lexmount-webfetch.zip" -@ +) echo "$dist_dir/lexmount-webfetch.zip" diff --git a/scripts/sign_and_notarize_macos.sh b/scripts/sign_and_notarize_macos.sh new file mode 100755 index 0000000..c07e5d1 --- /dev/null +++ b/scripts/sign_and_notarize_macos.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +binary="${1:?Usage: sign_and_notarize_macos.sh /path/to/binary}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${MACOS_DEVELOPER_ID_APPLICATION_P12_BASE64:?Missing MACOS_DEVELOPER_ID_APPLICATION_P12_BASE64}" +: "${MACOS_DEVELOPER_ID_P12_PASSWORD:?Missing MACOS_DEVELOPER_ID_P12_PASSWORD}" +: "${APPLE_NOTARY_APPLE_ID:?Missing APPLE_NOTARY_APPLE_ID}" +: "${APPLE_NOTARY_TEAM_ID:?Missing APPLE_NOTARY_TEAM_ID}" +: "${APPLE_NOTARY_APP_PASSWORD:?Missing APPLE_NOTARY_APP_PASSWORD}" + +[[ -f "${binary}" ]] || { echo "Binary not found: ${binary}" >&2; exit 2; } + +keychain_path="${RUNNER_TEMP}/lexmount-webfetch-cli-signing.keychain-db" +certificate_path="${RUNNER_TEMP}/developer_id_application.p12" +notary_archive="${RUNNER_TEMP}/$(basename "${binary}").notary.zip" +notary_response="${RUNNER_TEMP}/$(basename "${binary}").notarization.json" +notary_profile="lexmount-webfetch-cli-release-notary" +apple_pki_dir="${RUNNER_TEMP}/lexmount-webfetch-cli-apple-pki" +keychain_password="$(openssl rand -hex 24)" + +keychain_list="$(security list-keychains -d user 2>/dev/null || true)" +default_keychain="$(security default-keychain -d user 2>/dev/null | sed -E 's/^[[:space:]]*"(.*)"[[:space:]]*$/\1/' || true)" +existing_keychains=() +while IFS= read -r keychain; do + keychain="$(printf '%s' "${keychain}" | sed -E 's/^[[:space:]]*"(.*)"[[:space:]]*$/\1/; s/^[[:space:]]+//; s/[[:space:]]+$//')" + [[ -n "${keychain}" && -e "${keychain}" ]] && existing_keychains+=("${keychain}") +done <<< "${keychain_list}" + +cleanup() { + if [[ -n "${default_keychain}" && -e "${default_keychain}" ]]; then + security default-keychain -d user -s "${default_keychain}" >/dev/null 2>&1 || true + fi + if ((${#existing_keychains[@]} > 0)); then + security list-keychains -d user -s "${existing_keychains[@]}" >/dev/null 2>&1 || true + fi + security delete-keychain "${keychain_path}" >/dev/null 2>&1 || true + rm -f "${certificate_path}" "${notary_archive}" "${notary_response}" + rm -rf "${apple_pki_dir}" +} +trap cleanup EXIT INT TERM + +printf '%s' "${MACOS_DEVELOPER_ID_APPLICATION_P12_BASE64}" | base64 -D >"${certificate_path}" +security create-keychain -p "${keychain_password}" "${keychain_path}" +security set-keychain-settings -lut 21600 "${keychain_path}" +security unlock-keychain -p "${keychain_password}" "${keychain_path}" +security list-keychains -d user -s "${keychain_path}" "${existing_keychains[@]}" +security default-keychain -d user -s "${keychain_path}" + +mkdir -p "${apple_pki_dir}" +for certificate_spec in \ + 'AppleWWDRCAG3.cer|DCF21878C77F4198E4B4614F03D696D89C66C66008D4244E1B99161AAC91601F' \ + 'DeveloperIDCA.cer|7AFC9D01A62F03A2DE9637936D4AFE68090D2DE18D03F29C88CFB0B1BA63587F' \ + 'DeveloperIDG2CA.cer|F16CD3C54C7F83CEA4BF1A3E6A0819C8AAA8E4A1528FD144715F350643D2DF3A'; do + IFS='|' read -r certificate_name expected_fingerprint <<<"${certificate_spec}" + apple_certificate="${apple_pki_dir}/${certificate_name}" + curl --fail --silent --show-error --location --retry 3 --retry-delay 1 \ + --output "${apple_certificate}" \ + "https://www.apple.com/certificateauthority/${certificate_name}" + actual_fingerprint="$( + openssl x509 -inform der -in "${apple_certificate}" -noout -fingerprint -sha256 | + awk -F= '{print $2}' | tr -d ':' | tr '[:lower:]' '[:upper:]' + )" + [[ "${actual_fingerprint}" == "${expected_fingerprint}" ]] || { + echo "Unexpected Apple PKI certificate fingerprint: ${certificate_name}" >&2 + exit 3 + } + security import "${apple_certificate}" -k "${keychain_path}" -T /usr/bin/security >/dev/null +done + +security import "${certificate_path}" \ + -k "${keychain_path}" \ + -P "${MACOS_DEVELOPER_ID_P12_PASSWORD}" \ + -A \ + -T /usr/bin/codesign \ + -T /usr/bin/security +security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s \ + -k "${keychain_password}" \ + "${keychain_path}" + +identity="$(security find-identity -v -p codesigning "${keychain_path}" | awk -F'"' '/"Developer ID Application:/ {print $2; exit}')" +[[ -n "${identity}" ]] || { echo "Developer ID Application identity was not found" >&2; exit 4; } + +codesign --force --options runtime --timestamp --sign "${identity}" "${binary}" +codesign --verify --strict --verbose=4 "${binary}" +codesign -dv --verbose=4 "${binary}" 2>&1 | grep -F "Authority=Developer ID Application:" + +xcrun notarytool store-credentials "${notary_profile}" \ + --apple-id "${APPLE_NOTARY_APPLE_ID}" \ + --team-id "${APPLE_NOTARY_TEAM_ID}" \ + --password "${APPLE_NOTARY_APP_PASSWORD}" \ + --keychain "${keychain_path}" +ditto -c -k --keepParent "${binary}" "${notary_archive}" +xcrun notarytool submit "${notary_archive}" \ + --keychain-profile "${notary_profile}" \ + --keychain "${keychain_path}" \ + --wait \ + --output-format json >"${notary_response}" +cat "${notary_response}" +notary_status="$(plutil -extract status raw -o - "${notary_response}")" +[[ "${notary_status}" == "Accepted" ]] || { + echo "Apple notarization status was ${notary_status}" >&2 + exit 5 +} + +echo "Signed and notarized ${binary} with ${identity}" diff --git a/scripts/upload-release-to-cos.sh b/scripts/upload-release-to-cos.sh new file mode 100755 index 0000000..059e2a8 --- /dev/null +++ b/scripts/upload-release-to-cos.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +source_dir="${1:?Usage: upload-release-to-cos.sh SOURCE_DIR PRODUCT VERSION}" +product="${2:?Usage: upload-release-to-cos.sh SOURCE_DIR PRODUCT VERSION}" +version="${3:?Usage: upload-release-to-cos.sh SOURCE_DIR PRODUCT VERSION}" + +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${TENCENT_CLOUD_SECRET_ID:?Missing TENCENT_CLOUD_SECRET_ID}" +: "${TENCENT_CLOUD_SECRET_KEY:?Missing TENCENT_CLOUD_SECRET_KEY}" +: "${COS_BUCKET:?Missing COS_BUCKET}" +: "${COS_REGION:?Missing COS_REGION}" +: "${COS_PUBLIC_BASE_URL:?Missing COS_PUBLIC_BASE_URL}" +: "${COS_OBJECT_PREFIX:?Missing COS_OBJECT_PREFIX}" + +[[ -d "${source_dir}" ]] || { echo "Source directory not found: ${source_dir}" >&2; exit 2; } +[[ -f "${source_dir}/SHA256SUMS" ]] || { echo "Checksum manifest not found: ${source_dir}/SHA256SUMS" >&2; exit 2; } +[[ "${product}" =~ ^[a-z0-9-]+$ ]] || { echo "Invalid product path: ${product}" >&2; exit 2; } +[[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Invalid version: ${version}" >&2; exit 2; } + +coscli_version="1.0.8" +coscli_name="coscli-v${coscli_version}-linux-amd64" +coscli_path="${RUNNER_TEMP}/${coscli_name}" +coscli_sha256="7165f2ae16c5f7ac495864c963ca574a76e04ec72680d7bc8a8eee3234d8cf91" +verify_dir="${RUNNER_TEMP}/${product}-cos-verify" +object_prefix="${COS_OBJECT_PREFIX#/}" +object_prefix="${object_prefix%/}" +remote_path="${object_prefix}/${product}/v${version}" +public_url="${COS_PUBLIC_BASE_URL%/}/${remote_path}" + +curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location --retry 3 \ + "https://github.com/tencentyun/coscli/releases/download/v${coscli_version}/${coscli_name}" \ + --output "${coscli_path}" +printf '%s %s\n' "${coscli_sha256}" "${coscli_path}" | sha256sum --check --strict +chmod 0700 "${coscli_path}" + +"${coscli_path}" cp "${source_dir}/" "cos://${COS_BUCKET}/${remote_path}/" \ + --recursive \ + --endpoint "cos.${COS_REGION}.myqcloud.com" \ + --secret-id "${TENCENT_CLOUD_SECRET_ID}" \ + --secret-key "${TENCENT_CLOUD_SECRET_KEY}" \ + --init-skip=true \ + --disable-log + +mkdir -p "${verify_dir}" +curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location --retry 5 \ + "${public_url}/SHA256SUMS" --output "${verify_dir}/SHA256SUMS" +cmp "${source_dir}/SHA256SUMS" "${verify_dir}/SHA256SUMS" + +while read -r expected file_name; do + file_name="${file_name#\*}" + [[ "${file_name}" != */* && -n "${file_name}" ]] || { echo "Invalid manifest filename: ${file_name}" >&2; exit 3; } + curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location --retry 5 \ + "${public_url}/${file_name}" --output "${verify_dir}/${file_name}" + printf '%s %s\n' "${expected}" "${verify_dir}/${file_name}" | sha256sum --check --strict +done <"${source_dir}/SHA256SUMS" + +echo "Uploaded and publicly verified ${product} v${version} at ${public_url}" diff --git a/skills/lexmount-webfetch/SKILL.md b/skills/lexmount-webfetch/SKILL.md index 24fe84c..9373693 100644 --- a/skills/lexmount-webfetch/SKILL.md +++ b/skills/lexmount-webfetch/SKILL.md @@ -5,7 +5,12 @@ description: Use Lexmount WebFetch for lightweight public-page extraction and re # Lexmount WebFetch -Use `${CODEBUDDY_SKILL_DIR}/bin/webfetch-cli` on macOS or `${CODEBUDDY_SKILL_DIR}/bin/webfetch-cli.exe` on Windows. Release binaries support macOS ARM64 and Windows x64. The examples abbreviate that path as `webfetch-cli`. +Select the native Rust binary for the current platform: + +- macOS arm64: run `${CODEBUDDY_SKILL_DIR}/scripts/bootstrap.sh` when `${CODEBUDDY_SKILL_DIR}/bin/webfetch-cli` is missing, then use that file. +- Windows x64: run `${CODEBUDDY_SKILL_DIR}/scripts/bootstrap.ps1` when `${CODEBUDDY_SKILL_DIR}/bin/webfetch-cli.exe` is missing, then use that file. + +Both bootstrap scripts download the fixed release version from Tencent Cloud COS and verify its SHA-256 digest. The examples abbreviate the selected path as `webfetch-cli`. ## Fast path @@ -16,7 +21,7 @@ webfetch-cli extract --url webfetch-cli dump-dom --url ``` -Do not run setup checks before every extraction. Run the platform doctor script on first use or after an authentication/API error. If the binary is missing, run the matching bootstrap script after telling the user it downloads a fixed-version binary and verifies SHA-256. +Do not run setup checks before every extraction. On first use, run the matching bootstrap script if the binary is missing, then run the platform doctor script. Run doctor again after an authentication or API error. If credentials are missing, run `webfetch-cli auth login --open --client-name WorkBuddy`. Let the user approve in their browser; never ask them to paste an API key into chat. diff --git a/skills/lexmount-webfetch/scripts/bootstrap.ps1 b/skills/lexmount-webfetch/scripts/bootstrap.ps1 index 24457e1..0920129 100644 --- a/skills/lexmount-webfetch/scripts/bootstrap.ps1 +++ b/skills/lexmount-webfetch/scripts/bootstrap.ps1 @@ -1,14 +1,17 @@ $ErrorActionPreference = "Stop" -$version = if ($env:LEXMOUNT_WEBFETCH_CLI_VERSION) { $env:LEXMOUNT_WEBFETCH_CLI_VERSION } else { "0.1.0" } -if (-not [Environment]::Is64BitOperatingSystem) { throw "Only 64-bit Windows is supported" } +$version = if ($env:LEXMOUNT_WEBFETCH_CLI_VERSION) { $env:LEXMOUNT_WEBFETCH_CLI_VERSION } else { "0.1.1" } +$downloadBaseUrl = if ($env:LEXMOUNT_WEBFETCH_CLI_DOWNLOAD_BASE_URL) { $env:LEXMOUNT_WEBFETCH_CLI_DOWNLOAD_BASE_URL.TrimEnd('/') } else { "https://cli-bin-1377899528.cos.ap-nanjing.myqcloud.com/releases/webfetch-cli" } +$architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } +if ($architecture -ne "AMD64") { throw "Only Windows x64 is supported" } $asset = "webfetch-cli-v$version-x86_64-pc-windows-msvc.exe" -$repo = "https://github.com/lexmount/webfetch-cli-rs/releases/download/v$version" +$repo = "$downloadBaseUrl/v$version" $tmp = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString()) New-Item -ItemType Directory -Path $tmp | Out-Null try { Invoke-WebRequest -UseBasicParsing "$repo/$asset" -OutFile (Join-Path $tmp $asset) Invoke-WebRequest -UseBasicParsing "$repo/SHA256SUMS" -OutFile (Join-Path $tmp "SHA256SUMS") - $line = Get-Content (Join-Path $tmp "SHA256SUMS") | Where-Object { $_ -match "\s+$([regex]::Escape($asset))$" } | Select-Object -First 1 + # GNU sha256sum prefixes binary filenames with `*`; shasum uses plain whitespace. + $line = Get-Content (Join-Path $tmp "SHA256SUMS") | Where-Object { $_ -match "\s+\*?$([regex]::Escape($asset))$" } | Select-Object -First 1 if (-not $line) { throw "No checksum published for $asset" } $expected = ($line -split "\s+")[0].ToLowerInvariant() $actual = (Get-FileHash (Join-Path $tmp $asset) -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/skills/lexmount-webfetch/scripts/bootstrap.sh b/skills/lexmount-webfetch/scripts/bootstrap.sh index 1fda93b..f4873a2 100755 --- a/skills/lexmount-webfetch/scripts/bootstrap.sh +++ b/skills/lexmount-webfetch/scripts/bootstrap.sh @@ -1,8 +1,9 @@ #!/bin/sh set -eu -version="${LEXMOUNT_WEBFETCH_CLI_VERSION:-0.1.0}" -repo="https://github.com/lexmount/webfetch-cli-rs/releases/download/v${version}" +version="${LEXMOUNT_WEBFETCH_CLI_VERSION:-0.1.1}" +download_base_url="${LEXMOUNT_WEBFETCH_CLI_DOWNLOAD_BASE_URL:-https://cli-bin-1377899528.cos.ap-nanjing.myqcloud.com/releases/webfetch-cli}" +repo="${download_base_url%/}/v${version}" case "$(uname -s)-$(uname -m)" in Darwin-arm64) target="aarch64-apple-darwin" ;; *) echo "Unsupported platform: $(uname -s) $(uname -m). This release supports macOS ARM64 and Windows x64." >&2; exit 2 ;; diff --git a/skills/lexmount-webfetch/scripts/doctor.ps1 b/skills/lexmount-webfetch/scripts/doctor.ps1 index 2d40188..ebfad67 100644 --- a/skills/lexmount-webfetch/scripts/doctor.ps1 +++ b/skills/lexmount-webfetch/scripts/doctor.ps1 @@ -1,7 +1,10 @@ $ErrorActionPreference = "Stop" +$architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } +if ($architecture -ne "AMD64") { + Write-Output '{"ok":false,"error":"unsupported_platform","message":"This Skill supports Windows x64 through scripts/doctor.ps1 and macOS arm64 through scripts/doctor.sh."}' + exit 2 +} $skillBinary = Join-Path (Split-Path -Parent $PSScriptRoot) "bin\webfetch-cli.exe" if (Test-Path $skillBinary) { & $skillBinary doctor --json; exit $LASTEXITCODE } -$command = Get-Command webfetch-cli -ErrorAction SilentlyContinue -if (-not $command) { Write-Output '{"ok":false,"error":"command_not_found","message":"Run bootstrap.ps1 first."}'; exit 1 } -& webfetch-cli doctor --json -exit $LASTEXITCODE +Write-Output '{"ok":false,"error":"command_not_found","message":"Skill-local webfetch-cli.exe is missing. Run scripts/bootstrap.ps1 first."}' +exit 1 diff --git a/skills/lexmount-webfetch/scripts/doctor.sh b/skills/lexmount-webfetch/scripts/doctor.sh index 85bb232..cad579a 100755 --- a/skills/lexmount-webfetch/scripts/doctor.sh +++ b/skills/lexmount-webfetch/scripts/doctor.sh @@ -1,6 +1,10 @@ #!/bin/sh set -eu +case "$(uname -s)-$(uname -m)" in + Darwin-arm64) ;; + *) echo '{"ok":false,"error":"unsupported_platform","message":"This Skill supports macOS arm64 through scripts/doctor.sh and Windows x64 through scripts/doctor.ps1."}'; exit 2 ;; +esac skill_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" if [ -x "$skill_dir/bin/webfetch-cli" ]; then exec "$skill_dir/bin/webfetch-cli" doctor --json; fi -command -v webfetch-cli >/dev/null 2>&1 || { echo '{"ok":false,"error":"command_not_found","message":"Run bootstrap.sh first."}'; exit 1; } -exec webfetch-cli doctor --json +echo '{"ok":false,"error":"command_not_found","message":"Skill-local webfetch-cli is missing. Run scripts/bootstrap.sh first."}' +exit 1