fix: remove cargo-dist, use native fetch for npm installer (#646)
* chore: remove cargo-dist, use native fetch for npm installer * fix: harden npm scripts — spawnSync, signal handling, binary-exists check * fix: address PR review comments - Add null body guard for fetch response - Fix PowerShell Expand-Archive path quoting vulnerability - Sanitize error output to prevent ANSI escape injection - Add proxy support limitation note - Fix upgrade bug: use .version marker so npm update downloads new binary - Downgrade changeset from minor to patch (chore, not feature) - Update AGENTS.md: remove stale cargo-dist reference from labels * fix: use flat archives for consistent tar/zip extraction Both tar.gz and zip archives now contain files at root (no nested directory). Removes --strip-components 1 from install.js since it is no longer needed. This makes extraction consistent across platforms. --------- Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@googleworkspace/cli": patch
|
||||
---
|
||||
|
||||
Remove cargo-dist; use native Node.js fetch for npm binary installer
|
||||
|
||||
Replaces the cargo-dist generated release pipeline and npm package with:
|
||||
- A custom GitHub Actions release workflow with matrix cross-compilation
|
||||
- A zero-dependency npm installer using native `fetch()` (Node 18+)
|
||||
- Removes axios, rimraf, detect-libc, console.table, and axios-proxy-builder dependencies from the published npm package
|
||||
+132
-297
@@ -1,359 +1,194 @@
|
||||
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
|
||||
# Release workflow for @googleworkspace/cli
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
# Note that the GitHub Release will be created with a generated
|
||||
# title/body based on your changelogs.
|
||||
# Triggered by pushing a semver tag (e.g. v0.22.3).
|
||||
# Builds platform binaries, creates a GitHub Release, publishes to npm and crates.io.
|
||||
|
||||
name: Release
|
||||
permissions:
|
||||
"contents": "write"
|
||||
contents: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
# This task will run whenever you push a git tag that looks like a version
|
||||
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
# If you push multiple tags at once, separate instances of this workflow will
|
||||
# spin up, creating an independent announcement for each one. However, GitHub
|
||||
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||
# mistake.
|
||||
#
|
||||
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||
# will be marked as a prerelease.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-22.04"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||
publishing: ${{ !github.event.pull_request }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
prerelease: ${{ steps.meta.outputs.prerelease }}
|
||||
publishing: ${{ github.ref_type == 'tag' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
- id: meta
|
||||
run: |
|
||||
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
VERSION="${TAG#v}"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Build and packages all the platform-specific things
|
||||
build-local-artifacts:
|
||||
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||
# Let the initial task tell us to not run (currently very blunt)
|
||||
needs:
|
||||
- plan
|
||||
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||
build:
|
||||
needs: plan
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Target platforms/runners are computed by dist in create-release.
|
||||
# Each member of the matrix has the following arguments:
|
||||
#
|
||||
# - runner: the github runner
|
||||
# - dist-args: cli flags to pass to dist
|
||||
# - install-dist: expression to run to install dist on the runner
|
||||
#
|
||||
# Typically there will be:
|
||||
# - 1 "global" task that builds universal installers
|
||||
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-apple-darwin
|
||||
runner: macos-latest
|
||||
archive: tar.gz
|
||||
- target: x86_64-apple-darwin
|
||||
runner: macos-latest
|
||||
archive: tar.gz
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
runner: ubuntu-latest
|
||||
archive: tar.gz
|
||||
cross: true
|
||||
- target: aarch64-unknown-linux-musl
|
||||
runner: ubuntu-latest
|
||||
archive: tar.gz
|
||||
cross: true
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
runner: ubuntu-latest
|
||||
archive: tar.gz
|
||||
- target: x86_64-unknown-linux-musl
|
||||
runner: ubuntu-latest
|
||||
archive: tar.gz
|
||||
cross: true
|
||||
- target: x86_64-pc-windows-msvc
|
||||
runner: windows-latest
|
||||
archive: zip
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container: ${{ matrix.container && matrix.container.image || null }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||
permissions:
|
||||
"attestations": "write"
|
||||
"contents": "read"
|
||||
"id-token": "write"
|
||||
steps:
|
||||
- name: enable windows longpaths
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install Rust non-interactively if not already installed
|
||||
if: ${{ matrix.container }}
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install cross
|
||||
if: matrix.cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
if [ "${{ matrix.cross }}" = "true" ]; then
|
||||
cross build --release --target ${{ matrix.target }} --locked
|
||||
else
|
||||
cargo build --release --target ${{ matrix.target }} --locked
|
||||
fi
|
||||
- name: Install dist
|
||||
run: ${{ matrix.install_dist.run }}
|
||||
# Get the dist-manifest
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
|
||||
- name: Package (unix)
|
||||
if: matrix.archive == 'tar.gz'
|
||||
run: |
|
||||
${{ matrix.packages_install }}
|
||||
- name: Build artifacts
|
||||
ARTIFACT="google-workspace-cli-${{ matrix.target }}"
|
||||
mkdir -p staging
|
||||
cp "target/${{ matrix.target }}/release/gws" staging/
|
||||
cp LICENSE README.md CHANGELOG.md staging/
|
||||
tar czf "${ARTIFACT}.tar.gz" -C staging .
|
||||
shasum -a 256 "${ARTIFACT}.tar.gz" > "${ARTIFACT}.tar.gz.sha256"
|
||||
shell: bash
|
||||
|
||||
- name: Package (windows)
|
||||
if: matrix.archive == 'zip'
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
$ARTIFACT = "google-workspace-cli-${{ matrix.target }}"
|
||||
New-Item -ItemType Directory -Path $ARTIFACT
|
||||
Copy-Item "target/${{ matrix.target }}/release/gws.exe" "$ARTIFACT/"
|
||||
Copy-Item LICENSE, README.md, CHANGELOG.md "$ARTIFACT/"
|
||||
Compress-Archive -Path "$ARTIFACT/*" -DestinationPath "$ARTIFACT.zip"
|
||||
(Get-FileHash "$ARTIFACT.zip" -Algorithm SHA256).Hash.ToLower() + " $ARTIFACT.zip" | Out-File -Encoding ascii "$ARTIFACT.zip.sha256"
|
||||
shell: pwsh
|
||||
|
||||
- name: Attest
|
||||
if: github.ref_type == 'tag'
|
||||
uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3
|
||||
with:
|
||||
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
|
||||
- id: cargo-dist
|
||||
name: Post-build
|
||||
# We force bash here just because github makes it really hard to get values up
|
||||
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||
# inconsistent syntax between shell and powershell.
|
||||
shell: bash
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
subject-path: google-workspace-cli-${{ matrix.target }}.${{ matrix.archive }}
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||
name: binary-${{ matrix.target }}
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
google-workspace-cli-${{ matrix.target }}.${{ matrix.archive }}
|
||||
google-workspace-cli-${{ matrix.target }}.${{ matrix.archive }}.sha256
|
||||
|
||||
# Build and package all the platform-agnostic(ish) things
|
||||
build-global-artifacts:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
release:
|
||||
needs: [plan, build]
|
||||
if: github.ref_type == 'tag'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: binary-*
|
||||
path: artifacts/
|
||||
merge-multiple: true
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
# Determines if we should publish/announce
|
||||
host:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
- build-global-artifacts
|
||||
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||
RELEASE_COMMIT: "${{ github.sha }}"
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Write and read notes from a file to avoid quoting breaking things
|
||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
PRERELEASE_FLAG=""
|
||||
if [ "${{ needs.plan.outputs.prerelease }}" = "true" ]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
gh release create "${{ github.ref_name }}" \
|
||||
--target "${{ github.sha }}" \
|
||||
--title "${{ github.ref_name }}" \
|
||||
--generate-notes \
|
||||
$PRERELEASE_FLAG \
|
||||
artifacts/*
|
||||
|
||||
publish-npm:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PLAN: ${{ needs.plan.outputs.val }}
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
needs: [plan, release]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.plan.outputs.publishing == 'true' && needs.plan.outputs.prerelease == 'false' }}
|
||||
steps:
|
||||
- name: Fetch npm packages
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: npm/
|
||||
merge-multiple: true
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://wombat-dressing-room.appspot.com'
|
||||
- run: |
|
||||
for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith("-npm-package.tar.gz")] | any)'); do
|
||||
pkg=$(echo "$release" | jq '.artifacts[] | select(endswith("-npm-package.tar.gz"))' --raw-output)
|
||||
npm publish --access public "./npm/${pkg}"
|
||||
done
|
||||
|
||||
- name: Publish to npm
|
||||
working-directory: npm
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-cargo:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
runs-on: "ubuntu-22.04"
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
needs: [plan, release]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.plan.outputs.publishing == 'true' && needs.plan.outputs.prerelease == 'false' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
|
||||
# Publish library crate first (CLI depends on it)
|
||||
- name: Publish google-workspace
|
||||
run: cargo publish --package google-workspace
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
# Wait for crates.io to index the library crate
|
||||
- name: Wait for crates.io index
|
||||
run: sleep 30
|
||||
|
||||
- name: Publish google-workspace-cli
|
||||
run: cargo publish --package google-workspace-cli
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- publish-npm
|
||||
- publish-cargo
|
||||
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.publish-npm.result == 'skipped' || needs.publish-npm.result == 'success') && (needs.publish-cargo.result == 'skipped' || needs.publish-cargo.result == 'success') }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ Use these labels to categorize pull requests and issues:
|
||||
- `area: http` — Request execution, URL building, response handling
|
||||
- `area: docs` — README, contributing guides, documentation
|
||||
- `area: tui` — Setup wizard, picker, input fields
|
||||
- `area: distribution` — Nix flake, cargo-dist, npm packaging, install methods
|
||||
- `area: distribution` — Nix flake, npm packaging, GitHub Actions release workflow, install methods
|
||||
- `area: auth` — OAuth, credentials, multi-account, ADC
|
||||
- `area: skills` — AI skill generation and management
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
[workspace]
|
||||
members = ["cargo:crates/google-workspace-cli"]
|
||||
|
||||
# Config for 'cargo dist'
|
||||
[dist]
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.31.0"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
installers = ["shell", "powershell", "npm"]
|
||||
# Publish jobs to run
|
||||
publish-jobs = ["npm"]
|
||||
npm-scope = "@googleworkspace"
|
||||
# Enable github attestations
|
||||
github-attestations = true
|
||||
npm-package = "cli"
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "plan"
|
||||
# Don't overwrite release.yml on `cargo dist init` (preserves custom npm registry config)
|
||||
allow-dirty = ["ci"]
|
||||
# The archive format to use for windows builds (defaults .zip)
|
||||
# Using .zip routes through PowerShell's Expand-Archive, which correctly
|
||||
# handles Windows paths. Using .tar.gz causes failures in Git Bash because
|
||||
# MSYS tar interprets "C:" as a remote host (issue #152).
|
||||
windows-archive = ".zip"
|
||||
# The archive format to use for non-windows builds (defaults .tar.xz)
|
||||
unix-archive = ".tar.gz"
|
||||
@@ -0,0 +1,2 @@
|
||||
# Downloaded binary (created during npm postinstall)
|
||||
bin/
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const { pipeline } = require("stream/promises");
|
||||
const { createWriteStream, mkdirSync, rmSync } = require("fs");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { getPlatform } = require("./platform");
|
||||
|
||||
const INSTALL_DIR = path.join(__dirname, "bin");
|
||||
|
||||
/**
|
||||
* Get the GitHub release download URL base for the current package version.
|
||||
*/
|
||||
function getDownloadUrl(artifactName) {
|
||||
const { version } = require("./package.json");
|
||||
return `https://github.com/googleworkspace/cli/releases/download/v${version}/${artifactName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip ANSI escape sequences from a string.
|
||||
*/
|
||||
function sanitize(str) {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return String(str).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file using native fetch (Node 18+).
|
||||
*
|
||||
* NOTE: Native fetch does not respect HTTP_PROXY / HTTPS_PROXY environment
|
||||
* variables. If proxy support is needed, consider using the `undici` ProxyAgent
|
||||
* or a Node.js build with proxy support.
|
||||
*/
|
||||
async function download(url, dest) {
|
||||
const res = await fetch(url, { redirect: "follow" });
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
if (!res.body) {
|
||||
throw new Error(`Failed to download ${url}: Response body is empty`);
|
||||
}
|
||||
|
||||
const fileStream = createWriteStream(dest);
|
||||
// Convert web ReadableStream to Node stream and pipe
|
||||
const { Readable } = require("stream");
|
||||
const nodeStream = Readable.fromWeb(res.body);
|
||||
await pipeline(nodeStream, fileStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command and throw on failure.
|
||||
*/
|
||||
function run(cmd, args) {
|
||||
const result = spawnSync(cmd, args, { stdio: "pipe" });
|
||||
if (result.error) {
|
||||
throw new Error(`Failed to run ${cmd}: ${result.error.message}`);
|
||||
}
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
const stderr = result.stderr ? result.stderr.toString() : "";
|
||||
throw new Error(
|
||||
`Command failed: ${cmd} ${args.join(" ")}\n${stderr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the archive to the install directory.
|
||||
*/
|
||||
function extract(archivePath, destDir) {
|
||||
const isZip = archivePath.endsWith(".zip");
|
||||
const isTar = archivePath.includes(".tar.");
|
||||
|
||||
if (isTar) {
|
||||
run("tar", ["xf", archivePath, "-C", destDir]);
|
||||
} else if (isZip) {
|
||||
if (process.platform === "win32") {
|
||||
// Use single-quoted PowerShell strings with doubled single-quote escaping
|
||||
// to safely handle paths containing spaces and special characters.
|
||||
const psArchive = archivePath.replace(/'/g, "''");
|
||||
const psDest = destDir.replace(/'/g, "''");
|
||||
run("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
`Expand-Archive -LiteralPath '${psArchive}' -DestinationPath '${psDest}' -Force`,
|
||||
]);
|
||||
} else {
|
||||
run("unzip", ["-q", "-o", archivePath, "-d", destDir]);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported archive format: ${archivePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function install() {
|
||||
const platform = getPlatform();
|
||||
const { version } = require("./package.json");
|
||||
const url = getDownloadUrl(platform.artifact);
|
||||
|
||||
// Check if the correct version is already installed
|
||||
const binPath = path.join(INSTALL_DIR, platform.binary);
|
||||
const versionFile = path.join(INSTALL_DIR, ".version");
|
||||
if (fs.existsSync(binPath) && fs.existsSync(versionFile)) {
|
||||
const installed = fs.readFileSync(versionFile, "utf8").trim();
|
||||
if (installed === version) {
|
||||
console.error(`gws v${version} is already installed, skipping.`);
|
||||
return;
|
||||
}
|
||||
console.error(`Upgrading gws from v${installed} to v${version}`);
|
||||
}
|
||||
|
||||
// Clean and create install directory
|
||||
if (fs.existsSync(INSTALL_DIR)) {
|
||||
rmSync(INSTALL_DIR, { recursive: true, force: true });
|
||||
}
|
||||
mkdirSync(INSTALL_DIR, { recursive: true });
|
||||
|
||||
// Download to a temp file
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gws-"));
|
||||
const archiveName = path.basename(platform.artifact);
|
||||
const tmpFile = path.join(tmpDir, archiveName);
|
||||
|
||||
try {
|
||||
console.error(`Downloading gws from ${url}`);
|
||||
await download(url, tmpFile);
|
||||
|
||||
console.error(`Extracting to ${INSTALL_DIR}`);
|
||||
extract(tmpFile, INSTALL_DIR);
|
||||
|
||||
// Make binary executable on Unix
|
||||
if (process.platform !== "win32") {
|
||||
fs.chmodSync(binPath, 0o755);
|
||||
}
|
||||
|
||||
console.error(`gws v${version} has been installed!`);
|
||||
fs.writeFileSync(versionFile, version);
|
||||
} finally {
|
||||
// Clean up temp files
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
install().catch((err) => {
|
||||
console.error(`Error installing gws: ${sanitize(err.message)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "@googleworkspace/cli",
|
||||
"description": "Google Workspace CLI — dynamic command surface from Discovery Service",
|
||||
"version": "0.22.3",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Justin Poehnelt",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleworkspace/cli.git"
|
||||
},
|
||||
"homepage": "https://github.com/googleworkspace/cli",
|
||||
"bugs": {
|
||||
"url": "https://github.com/googleworkspace/cli/issues"
|
||||
},
|
||||
"bin": {
|
||||
"gws": "run.js"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node install.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
"keywords": [
|
||||
"cli",
|
||||
"google-workspace",
|
||||
"google",
|
||||
"google-api",
|
||||
"google-drive",
|
||||
"google-gmail",
|
||||
"google-sheets",
|
||||
"google-calendar",
|
||||
"google-docs",
|
||||
"google-chat",
|
||||
"google-admin",
|
||||
"gsuite",
|
||||
"discovery-api",
|
||||
"ai-agent",
|
||||
"agent-skills",
|
||||
"automation",
|
||||
"oauth2",
|
||||
"rust"
|
||||
],
|
||||
"publishConfig": {
|
||||
"provenance": true,
|
||||
"registry": "https://wombat-dressing-room.appspot.com"
|
||||
},
|
||||
"supportedPlatforms": {
|
||||
"aarch64-apple-darwin": {
|
||||
"artifact": "google-workspace-cli-aarch64-apple-darwin.tar.gz",
|
||||
"binary": "gws"
|
||||
},
|
||||
"x86_64-apple-darwin": {
|
||||
"artifact": "google-workspace-cli-x86_64-apple-darwin.tar.gz",
|
||||
"binary": "gws"
|
||||
},
|
||||
"aarch64-unknown-linux-gnu": {
|
||||
"artifact": "google-workspace-cli-aarch64-unknown-linux-gnu.tar.gz",
|
||||
"binary": "gws"
|
||||
},
|
||||
"aarch64-unknown-linux-musl": {
|
||||
"artifact": "google-workspace-cli-aarch64-unknown-linux-musl.tar.gz",
|
||||
"binary": "gws"
|
||||
},
|
||||
"x86_64-unknown-linux-gnu": {
|
||||
"artifact": "google-workspace-cli-x86_64-unknown-linux-gnu.tar.gz",
|
||||
"binary": "gws"
|
||||
},
|
||||
"x86_64-unknown-linux-musl": {
|
||||
"artifact": "google-workspace-cli-x86_64-unknown-linux-musl.tar.gz",
|
||||
"binary": "gws"
|
||||
},
|
||||
"x86_64-pc-windows-msvc": {
|
||||
"artifact": "google-workspace-cli-x86_64-pc-windows-msvc.zip",
|
||||
"binary": "gws.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const { supportedPlatforms } = require("./package.json");
|
||||
|
||||
/**
|
||||
* Map Node.js os.type() and os.arch() to Rust-style target triples.
|
||||
*/
|
||||
function getPlatformKey() {
|
||||
const rawOs = os.type();
|
||||
const rawArch = os.arch();
|
||||
|
||||
let osType;
|
||||
switch (rawOs) {
|
||||
case "Windows_NT":
|
||||
osType = "pc-windows-msvc";
|
||||
break;
|
||||
case "Darwin":
|
||||
osType = "apple-darwin";
|
||||
break;
|
||||
case "Linux":
|
||||
osType = "unknown-linux-gnu";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported operating system: ${rawOs}`);
|
||||
}
|
||||
|
||||
let arch;
|
||||
switch (rawArch) {
|
||||
case "x64":
|
||||
arch = "x86_64";
|
||||
break;
|
||||
case "arm64":
|
||||
arch = "aarch64";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture: ${rawArch}`);
|
||||
}
|
||||
|
||||
// On Linux, try to detect musl libc
|
||||
if (rawOs === "Linux") {
|
||||
try {
|
||||
const result = spawnSync("ldd", ["--version"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
// musl ldd prints version info to stderr
|
||||
const output = (result.stdout || "") + (result.stderr || "");
|
||||
if (output.toLowerCase().includes("musl")) {
|
||||
osType = "unknown-linux-musl";
|
||||
}
|
||||
} catch {
|
||||
// If ldd fails, assume glibc
|
||||
}
|
||||
}
|
||||
|
||||
const key = `${arch}-${osType}`;
|
||||
|
||||
if (!supportedPlatforms[key]) {
|
||||
// Try musl fallback on Linux if glibc binary is not available
|
||||
if (rawOs === "Linux") {
|
||||
const muslKey = `${arch}-unknown-linux-musl`;
|
||||
if (supportedPlatforms[muslKey]) {
|
||||
return muslKey;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupported platform: ${key}\nSupported platforms: ${Object.keys(supportedPlatforms).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function getPlatform() {
|
||||
const key = getPlatformKey();
|
||||
return supportedPlatforms[key];
|
||||
}
|
||||
|
||||
module.exports = { getPlatform, getPlatformKey };
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { getPlatform } = require("./platform");
|
||||
|
||||
const platform = getPlatform();
|
||||
const binPath = path.join(__dirname, "bin", platform.binary);
|
||||
|
||||
if (!fs.existsSync(binPath)) {
|
||||
console.error(
|
||||
`gws binary not found at ${binPath}\nRun "npm install -g @googleworkspace/cli" to install it.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = spawnSync(binPath, process.argv.slice(2), {
|
||||
cwd: process.cwd(),
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Error running gws: ${result.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -26,6 +26,13 @@ done
|
||||
sed -i.bak -E "s/(google-workspace = \{ version = \")[^\"]+/\1${VERSION}/" crates/google-workspace-cli/Cargo.toml
|
||||
rm -f crates/google-workspace-cli/Cargo.toml.bak
|
||||
|
||||
# Update npm installer package.json version
|
||||
node -e "
|
||||
const pkg = require('./npm/package.json');
|
||||
pkg.version = '${VERSION}';
|
||||
require('fs').writeFileSync('./npm/package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
|
||||
# Update Cargo.lock to match
|
||||
cargo generate-lockfile
|
||||
|
||||
@@ -38,5 +45,5 @@ fi
|
||||
cargo run -- generate-skills --output-dir skills
|
||||
|
||||
# Stage the changed files so changesets/action commits them
|
||||
git add crates/*/Cargo.toml Cargo.lock flake.nix flake.lock skills/
|
||||
git add crates/*/Cargo.toml Cargo.lock flake.nix flake.lock skills/ npm/package.json
|
||||
|
||||
|
||||
Reference in New Issue
Block a user