From 387887da10c2c0859c085bf62aba25b2e6531004 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 11 Jun 2026 19:51:41 -0400 Subject: [PATCH 01/11] Add v0.4.0-dev.1 preview: native binary distribution - New tag-triggered release workflow builds dart_skills_lint as a standalone native binary for macOS arm64/x64 and Linux x64/arm64 via `dart compile exe`, packages each as a tarball with SHA256, and cuts a GitHub Release with release notes extracted from CHANGELOG.md. - New install.sh detects OS/arch, downloads the matching tarball, verifies SHA256, and installs to INSTALL_DIR (default /usr/local/bin with sudo fallback). REPO/VERSION/INSTALL_DIR are env-configurable so the script survives the impending dart_skills_lint repo move with a single default-value edit. - Pub.dev install paths (`dart pub global activate` and dev_dependency) are unchanged; binaries are a parallel channel. - macOS binaries in this preview are unsigned. Homebrew formula is deferred until the new home repo is settled to avoid forcing early adopters through a re-tap on migration. --- .../workflows/dart_skills_lint_release.yaml | 155 ++++++++++++++++++ tool/dart_skills_lint/CHANGELOG.md | 34 ++++ tool/dart_skills_lint/pubspec.yaml | 2 +- tool/dart_skills_lint/scripts/install.sh | 132 +++++++++++++++ 4 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/dart_skills_lint_release.yaml create mode 100755 tool/dart_skills_lint/scripts/install.sh diff --git a/.github/workflows/dart_skills_lint_release.yaml b/.github/workflows/dart_skills_lint_release.yaml new file mode 100644 index 0000000..ccd9c69 --- /dev/null +++ b/.github/workflows/dart_skills_lint_release.yaml @@ -0,0 +1,155 @@ +name: dart_skills_lint release +permissions: read-all + +on: + push: + tags: + - 'dart_skills_lint-v*' + +jobs: + build_binaries: + strategy: + fail-fast: false + matrix: + include: + - os: macos-14 + target: macos-arm64 + - os: macos-13 + target: macos-x64 + - os: ubuntu-22.04 + target: linux-x64 + - os: ubuntu-22.04-arm + target: linux-arm64 + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: tool/dart_skills_lint + shell: bash + steps: + - uses: actions/checkout@v6 + + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Resolve dependencies + run: dart pub get + + - name: Compile native binary + run: | + set -euo pipefail + mkdir -p dist + dart compile exe bin/cli.dart -o "dist/dart_skills_lint-${TARGET}" + chmod +x "dist/dart_skills_lint-${TARGET}" + env: + TARGET: ${{ matrix.target }} + + - name: Smoke-test binary + run: ./dist/dart_skills_lint-${{ matrix.target }} --help + + - name: Package and hash + run: | + set -euo pipefail + cd dist + tar -czf "dart_skills_lint-${TARGET}.tar.gz" "dart_skills_lint-${TARGET}" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "dart_skills_lint-${TARGET}.tar.gz" > "dart_skills_lint-${TARGET}.tar.gz.sha256" + else + shasum -a 256 "dart_skills_lint-${TARGET}.tar.gz" > "dart_skills_lint-${TARGET}.tar.gz.sha256" + fi + env: + TARGET: ${{ matrix.target }} + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: dart_skills_lint-${{ matrix.target }} + path: | + tool/dart_skills_lint/dist/dart_skills_lint-${{ matrix.target }}.tar.gz + tool/dart_skills_lint/dist/dart_skills_lint-${{ matrix.target }}.tar.gz.sha256 + if-no-files-found: error + retention-days: 7 + + release: + needs: build_binaries + runs-on: ubuntu-latest + permissions: + contents: write + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v6 + + - name: Download all binary artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Build SHA256SUMS aggregate file + working-directory: dist + run: | + set -euo pipefail + : > SHA256SUMS + for f in *.tar.gz.sha256; do + cat "$f" >> SHA256SUMS + done + rm -f *.tar.gz.sha256 + echo "--- SHA256SUMS ---" + cat SHA256SUMS + + - name: Extract release metadata + id: meta + run: | + set -euo pipefail + # Tag pattern: dart_skills_lint-v. Strip prefix to get the version. + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#dart_skills_lint-v}" + if [ "$VERSION" = "$TAG" ]; then + echo "ERROR: tag '$TAG' does not match expected pattern 'dart_skills_lint-v'" >&2 + exit 1 + fi + # Verify pubspec matches. + PUBSPEC_VERSION=$(awk -F': *' '/^version:/ {print $2; exit}' tool/dart_skills_lint/pubspec.yaml) + if [ "$VERSION" != "$PUBSPEC_VERSION" ]; then + echo "ERROR: tag version '$VERSION' does not match pubspec.yaml version '$PUBSPEC_VERSION'" >&2 + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + # Pre-release detection for the GitHub Release flag. + case "$VERSION" in + *-dev.*|*-alpha.*|*-beta.*|*-rc.*|*-preview.*) echo "prerelease=true" >> "$GITHUB_OUTPUT" ;; + *) echo "prerelease=false" >> "$GITHUB_OUTPUT" ;; + esac + + - name: Extract release notes from CHANGELOG + run: | + set -euo pipefail + awk -v ver="## ${VERSION}" ' + $0 == ver { found = 1; next } + /^## / && found { exit } + found { print } + ' tool/dart_skills_lint/CHANGELOG.md > release-notes.md + if [ ! -s release-notes.md ]; then + echo "ERROR: no CHANGELOG entry found for version '${VERSION}'" >&2 + echo "Expected a heading line: ## ${VERSION}" >&2 + exit 1 + fi + echo "--- release notes ---" + cat release-notes.md + env: + VERSION: ${{ steps.meta.outputs.version }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: dart_skills_lint v${{ steps.meta.outputs.version }} + body_path: release-notes.md + prerelease: ${{ steps.meta.outputs.prerelease }} + files: | + dist/*.tar.gz + dist/SHA256SUMS + tool/dart_skills_lint/scripts/install.sh + fail_on_unmatched_files: true diff --git a/tool/dart_skills_lint/CHANGELOG.md b/tool/dart_skills_lint/CHANGELOG.md index 27337b3..9e94d0b 100644 --- a/tool/dart_skills_lint/CHANGELOG.md +++ b/tool/dart_skills_lint/CHANGELOG.md @@ -1,3 +1,37 @@ +## 0.4.0-dev.1 + +**Pre-release.** Pub.dev consumers with caret ranges (`^0.3.0` or `^0.4.0`) +will not auto-pick this up; pin explicitly (`dart_skills_lint: 0.4.0-dev.1`) +to try the preview. + +### Distribution + +- Native binaries are now published to GitHub Releases for macOS arm64, + macOS x64, Linux x64, and Linux arm64. Users without the Dart SDK on + PATH can install the linter via `install.sh` or a direct `curl -L` of + the release tarball — no `dart pub global activate` required. +- New `tool/dart_skills_lint/scripts/install.sh`: detects OS/arch, + downloads the matching tarball from the latest release, verifies its + SHA256 against the release's `SHA256SUMS`, and installs the binary to + `INSTALL_DIR` (default `/usr/local/bin`, with a sudo fallback). + `REPO`, `VERSION`, and `INSTALL_DIR` are env-configurable. +- New `.github/workflows/dart_skills_lint_release.yaml`: tag-triggered + workflow that builds the four-target matrix with `dart compile exe`, + packages each as `dart_skills_lint-.tar.gz`, generates a + `SHA256SUMS` aggregate, and cuts a GitHub Release whose body is + extracted from this CHANGELOG entry. +- The pub.dev install paths (`dart pub global activate dart_skills_lint` + and `dev_dependencies:`) are unchanged. Native binaries are a parallel + channel, not a replacement. +- macOS binaries in this preview are **not yet code-signed**. Users will + see a Gatekeeper warning on first launch. Workaround: + `xattr -d com.apple.quarantine $(which dart_skills_lint)`. Signing + lands once the org Developer ID Application cert is approved. +- Homebrew formula is **deferred**: `dart_skills_lint` is being moved to + a dedicated repo (location TBD), and a `brew tap` would force every + early adopter through a re-tap on migration. The formula ships from + the new repo once it exists. + ## 0.3.1 - `--fix` now writes fixes to disk; pair with `--dry-run` diff --git a/tool/dart_skills_lint/pubspec.yaml b/tool/dart_skills_lint/pubspec.yaml index e528b20..95be53a 100644 --- a/tool/dart_skills_lint/pubspec.yaml +++ b/tool/dart_skills_lint/pubspec.yaml @@ -3,7 +3,7 @@ description: >- A static analysis linter for Agent Skills (SKILL.md) written in Dart. Validates frontmatter, naming, paths, and structure for use in CI and pre-commit hooks. -version: 0.3.1 +version: 0.4.0-dev.1 resolution: workspace repository: https://github.com/flutter/skills issue_tracker: https://github.com/flutter/skills/issues diff --git a/tool/dart_skills_lint/scripts/install.sh b/tool/dart_skills_lint/scripts/install.sh new file mode 100755 index 0000000..992850f --- /dev/null +++ b/tool/dart_skills_lint/scripts/install.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# +# install.sh — Install the dart_skills_lint native binary. +# +# Usage (default repo + latest version): +# curl -fsSL https://github.com/flutter/skills/releases/latest/download/install.sh | bash +# +# Pin a specific version or alternate repo: +# curl -fsSL .../install.sh | REPO=other-org/other-repo VERSION=0.4.0-dev.1 bash +# +# Env vars: +# REPO GitHub owner/repo (default: flutter/skills). +# VERSION "latest" or a specific version like 0.4.0-dev.1 (default: latest). +# INSTALL_DIR Install destination (default: /usr/local/bin). + +set -euo pipefail + +REPO="${REPO:-flutter/skills}" +VERSION="${VERSION:-latest}" +INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" +BIN_NAME="dart_skills_lint" + +err() { echo "install.sh: error: $*" >&2; exit 1; } +info() { echo "install.sh: $*"; } + +# --- Detect platform --------------------------------------------------------- +case "$(uname -s)" in + Darwin) os="macos" ;; + Linux) os="linux" ;; + *) err "unsupported OS '$(uname -s)'. Supported: macOS, Linux." ;; +esac + +case "$(uname -m)" in + arm64|aarch64) arch="arm64" ;; + x86_64|amd64) arch="x64" ;; + *) err "unsupported architecture '$(uname -m)'. Supported: arm64, aarch64, x86_64, amd64." ;; +esac + +target="${os}-${arch}" +case "$target" in + macos-arm64|macos-x64|linux-x64|linux-arm64) ;; + *) err "no published binary for platform '${target}'. Available: macos-arm64, macos-x64, linux-x64, linux-arm64." ;; +esac + +# --- Required tools --------------------------------------------------------- +require() { command -v "$1" >/dev/null 2>&1 || err "required tool '$1' not found on PATH."; } +require curl +require tar + +if command -v sha256sum >/dev/null 2>&1; then + shasum_cmd() { sha256sum "$@"; } +elif command -v shasum >/dev/null 2>&1; then + shasum_cmd() { shasum -a 256 "$@"; } +else + err "neither 'sha256sum' nor 'shasum' is available. Install one to verify the binary." +fi + +# --- Resolve URLs ---------------------------------------------------------- +if [ "$VERSION" = "latest" ]; then + base_url="https://github.com/${REPO}/releases/latest/download" +else + tag="dart_skills_lint-v${VERSION}" + base_url="https://github.com/${REPO}/releases/download/${tag}" +fi +archive="${BIN_NAME}-${target}.tar.gz" +archive_url="${base_url}/${archive}" +sums_url="${base_url}/SHA256SUMS" + +# --- Download into a tempdir, cleaned up on exit ----------------------------- +tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dart-skills-lint-install.XXXXXX")" +trap 'rm -rf "$tmpdir"' EXIT INT TERM + +info "downloading ${archive} from ${REPO} (${VERSION})" +curl -fsSL --retry 3 -o "${tmpdir}/${archive}" "$archive_url" \ + || err "could not download ${archive_url}" +curl -fsSL --retry 3 -o "${tmpdir}/SHA256SUMS" "$sums_url" \ + || err "could not download ${sums_url}" + +# --- Verify SHA256 ---------------------------------------------------------- +expected_sha="$(awk -v fname="$archive" '$2 == fname { print $1; exit }' "${tmpdir}/SHA256SUMS")" +[ -n "$expected_sha" ] || err "no SHA256 entry for '${archive}' in SHA256SUMS." + +actual_sha="$(shasum_cmd "${tmpdir}/${archive}" | awk '{print $1}')" +if [ "$expected_sha" != "$actual_sha" ]; then + err "SHA256 mismatch for ${archive}. Expected ${expected_sha}, got ${actual_sha}." +fi +info "checksum verified" + +# --- Extract ---------------------------------------------------------------- +( cd "$tmpdir" && tar -xzf "$archive" ) +extracted="${tmpdir}/${BIN_NAME}-${target}" +[ -x "$extracted" ] || err "extracted file ${extracted} not found or not executable." + +# --- Install ---------------------------------------------------------------- +install_path="${INSTALL_DIR}/${BIN_NAME}" + +needs_sudo=0 +if [ -d "$INSTALL_DIR" ]; then + [ -w "$INSTALL_DIR" ] || needs_sudo=1 +else + parent="$(dirname "$INSTALL_DIR")" + [ -d "$parent" ] && [ -w "$parent" ] || needs_sudo=1 +fi + +if [ "$needs_sudo" = "0" ]; then + mkdir -p "$INSTALL_DIR" + install -m 0755 "$extracted" "$install_path" +elif command -v sudo >/dev/null 2>&1; then + info "${INSTALL_DIR} is not writable; using sudo" + sudo mkdir -p "$INSTALL_DIR" + sudo install -m 0755 "$extracted" "$install_path" +else + err "${INSTALL_DIR} is not writable and 'sudo' is not available. Set INSTALL_DIR to a writable path and re-run." +fi + +# --- Verify the installed binary launches ----------------------------------- +"$install_path" --help >/dev/null 2>&1 \ + || err "installed binary at ${install_path} failed to launch." + +info "installed ${BIN_NAME} → ${install_path}" +info "run '${BIN_NAME} --help' to get started" + +# --- macOS Gatekeeper note (preview binaries are unsigned) ------------------ +if [ "$os" = "macos" ]; then + cat < Date: Thu, 11 Jun 2026 19:57:06 -0400 Subject: [PATCH 02/11] Bump GitHub Actions to Node 24-compatible versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actions/upload-artifact: v4 → v7 (was the Node 20 deprecation warning source from the first fork test) - actions/download-artifact: v4 → v8 - softprops/action-gh-release: v2 → v3 All three were on Node 20, which GitHub forces to Node 24 on 2026-06-16. Each matrix job uploads with a unique artifact name, so v5+'s duplicate-name restriction is non-issue. download-artifact v8's new error-on-hash-mismatch default is a security upgrade. --- .github/workflows/dart_skills_lint_release.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dart_skills_lint_release.yaml b/.github/workflows/dart_skills_lint_release.yaml index ccd9c69..e0f7e2a 100644 --- a/.github/workflows/dart_skills_lint_release.yaml +++ b/.github/workflows/dart_skills_lint_release.yaml @@ -61,7 +61,7 @@ jobs: TARGET: ${{ matrix.target }} - name: Upload binary artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dart_skills_lint-${{ matrix.target }} path: | @@ -82,7 +82,7 @@ jobs: - uses: actions/checkout@v6 - name: Download all binary artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: dist merge-multiple: true @@ -142,7 +142,7 @@ jobs: VERSION: ${{ steps.meta.outputs.version }} - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} name: dart_skills_lint v${{ steps.meta.outputs.version }} From d1f1dd4c5506c2b4adb65eb8e5b3d64a8430c2c2 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 11 Jun 2026 20:19:39 -0400 Subject: [PATCH 03/11] Update README install section for v0.4.0-dev.1 binary preview Lead with install.sh as the recommended path; add a direct-curl variant for environments that don't pipe scripts to bash. Keep the pub.dev paths (dev_dependency and `dart pub global activate`) unchanged under a "Dart developers" section. Documents the macOS Gatekeeper workaround for unsigned preview binaries, a brief note that Homebrew is coming after the imminent repo migration, and the pinning syntax to opt into the preview track. Bumps the stable caret range example from ^0.2.0 to ^0.3.0. --- tool/dart_skills_lint/README.md | 83 ++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md index 834816a..8e6df89 100644 --- a/tool/dart_skills_lint/README.md +++ b/tool/dart_skills_lint/README.md @@ -25,21 +25,90 @@ For a full definition of the skill standard, see the [Agent Skills Specification ## Installation -Add `dart_skills_lint` to your Dart project or activate it globally. +`dart_skills_lint` ships as both a standalone native binary (no Dart +SDK required) and as a Dart package on pub.dev. Pick the path that +matches your environment. -### 1. As a project dependency -Add it to your `pubspec.yaml` (once published on pub.dev): +> **Homebrew note.** A `brew install dart-skills-lint` path is on the +> roadmap; it will land after `dart_skills_lint` migrates to its own +> dedicated repository. Until then, the install paths below cover all +> supported platforms. + +### 1. `install.sh` — Linux + macOS, no Dart required + +The recommended path for CI runners and laptops without the Dart SDK +on PATH. Downloads the matching prebuilt binary from the latest GitHub +Release, verifies its SHA256, and installs to `/usr/local/bin` (with a +`sudo` fallback). Supports macOS arm64 + x64 and Linux x64 + arm64. + +```bash +curl -fsSL https://github.com/flutter/skills/releases/latest/download/install.sh | bash +``` + +Optional env vars (set before the `bash` part): +- `INSTALL_DIR` — install destination (default `/usr/local/bin`). +- `VERSION` — pin a specific release like `0.4.0-dev.1` (default `latest`). +- `REPO` — alternate source repo (default `flutter/skills`). + +#### macOS first-launch note + +Preview binaries are not yet code-signed. The first time you run the +binary, macOS Gatekeeper will block it ("cannot be opened because the +developer cannot be verified"). Remove the quarantine flag once: + +```bash +xattr -d com.apple.quarantine "$(which dart_skills_lint)" +``` + +This step goes away once notarized builds ship. + +### 2. Direct download — Linux + macOS, no install script + +For environments where piping a script to `bash` isn't acceptable. +Grab the tarball for your platform from +[the latest GitHub Release](https://github.com/flutter/skills/releases/latest) +and verify its SHA256 against the release's `SHA256SUMS` asset. + +```bash +TARGET="linux-x64" # or: macos-arm64, macos-x64, linux-arm64 +VERSION="0.4.0-dev.1" +BASE="https://github.com/flutter/skills/releases/download/dart_skills_lint-v${VERSION}" +curl -fsSLO "${BASE}/dart_skills_lint-${TARGET}.tar.gz" +curl -fsSLO "${BASE}/SHA256SUMS" +grep " dart_skills_lint-${TARGET}.tar.gz$" SHA256SUMS | sha256sum -c - +tar -xzf "dart_skills_lint-${TARGET}.tar.gz" +sudo install -m 0755 "dart_skills_lint-${TARGET}" /usr/local/bin/dart_skills_lint +``` + +On macOS, replace `sha256sum -c -` with `shasum -a 256 -c -`. + +### 3. Dart developers — pub.dev + +If you already have the Dart SDK installed, the standard pub.dev paths +still work and are unchanged. + +#### As a project dev_dependency + +Add to your `pubspec.yaml`: ```yaml dev_dependencies: - dart_skills_lint: ^0.2.0 + dart_skills_lint: ^0.3.0 ``` -Then run: + +To opt into the preview track, pin to the exact version: +```yaml +dev_dependencies: + dart_skills_lint: 0.4.0-dev.1 +``` + +Then: ```bash dart pub get ``` -### 2. Globally activated -If you want to use it across multiple projects without adding it to each `pubspec.yaml`: +#### Globally activated + +For multiple projects without per-project pubspec entries: ```bash dart pub global activate dart_skills_lint ``` From 76ff6327c9fe99521a9c818942da1f1d9d74fea8 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 11 Jun 2026 20:38:38 -0400 Subject: [PATCH 04/11] Address Gemini review on install.sh - install.sh:118 (high): print the macOS Gatekeeper note BEFORE the --help launch check so users see the xattr workaround even when Gatekeeper blocks the binary. On macOS, downgrade the --help failure from a hard err to an informational message since the install itself succeeded; the launch will work once quarantine is cleared. Non-macOS still hard-errs on launch failure. - install.sh:80 (medium): strip leading '*' from SHA256SUMS field 2 before comparison so binary-mode hash files (sha256sum -b output) work as well as text-mode. --- tool/dart_skills_lint/scripts/install.sh | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/tool/dart_skills_lint/scripts/install.sh b/tool/dart_skills_lint/scripts/install.sh index 992850f..9fc858d 100755 --- a/tool/dart_skills_lint/scripts/install.sh +++ b/tool/dart_skills_lint/scripts/install.sh @@ -77,7 +77,12 @@ curl -fsSL --retry 3 -o "${tmpdir}/SHA256SUMS" "$sums_url" \ || err "could not download ${sums_url}" # --- Verify SHA256 ---------------------------------------------------------- -expected_sha="$(awk -v fname="$archive" '$2 == fname { print $1; exit }' "${tmpdir}/SHA256SUMS")" +# Strip the optional leading '*' that `sha256sum -b` (binary mode) puts before +# the filename, so SHA256SUMS files from either text or binary mode work. +expected_sha="$(awk -v fname="$archive" ' + { sub(/^\*/, "", $2) } + $2 == fname { print $1; exit } +' "${tmpdir}/SHA256SUMS")" [ -n "$expected_sha" ] || err "no SHA256 entry for '${archive}' in SHA256SUMS." actual_sha="$(shasum_cmd "${tmpdir}/${archive}" | awk '{print $1}')" @@ -113,14 +118,9 @@ else err "${INSTALL_DIR} is not writable and 'sudo' is not available. Set INSTALL_DIR to a writable path and re-run." fi -# --- Verify the installed binary launches ----------------------------------- -"$install_path" --help >/dev/null 2>&1 \ - || err "installed binary at ${install_path} failed to launch." - -info "installed ${BIN_NAME} → ${install_path}" -info "run '${BIN_NAME} --help' to get started" - # --- macOS Gatekeeper note (preview binaries are unsigned) ------------------ +# Print BEFORE the launch check so users see the workaround even if Gatekeeper +# blocks the --help invocation below. if [ "$os" = "macos" ]; then cat </dev/null 2>&1; then + info "installed ${BIN_NAME} → ${install_path}" + info "run '${BIN_NAME} --help' to get started" +elif [ "$os" = "macos" ]; then + info "installed ${BIN_NAME} → ${install_path}" + info "launch check failed — likely Gatekeeper. See the note above to clear quarantine, then run '${BIN_NAME} --help'." +else + err "installed binary at ${install_path} failed to launch." +fi From 4120617bcc4729b74f5b4d7d35d0eb4a7ee6981d Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Fri, 12 Jun 2026 17:54:10 -0400 Subject: [PATCH 05/11] Address self-review on PR #158 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install.sh: collapse three hand-rolled "Supported: ..." messages behind one SUPPORTED_TARGETS constant, so the error text and the final platform check share a source of truth. Error text now lists normalized targets (macos-arm64, macos-x64, linux-x64, linux-arm64) instead of raw uname variants. - CHANGELOG.md: shrink the 0.4.0-dev.1 entry to match the 0.3.1 style — flat user-facing bullets, no internal workflow detail or Homebrew roadmap. - README.md: reorder the Installation section so the pub.dev path (existing Dart audience) comes first, followed by install.sh and the direct-download path for the no-Dart preview audience. --- tool/dart_skills_lint/CHANGELOG.md | 43 +++++---------- tool/dart_skills_lint/README.md | 66 ++++++++++++------------ tool/dart_skills_lint/scripts/install.sh | 16 ++++-- 3 files changed, 56 insertions(+), 69 deletions(-) diff --git a/tool/dart_skills_lint/CHANGELOG.md b/tool/dart_skills_lint/CHANGELOG.md index 9e94d0b..7974098 100644 --- a/tool/dart_skills_lint/CHANGELOG.md +++ b/tool/dart_skills_lint/CHANGELOG.md @@ -1,36 +1,17 @@ ## 0.4.0-dev.1 -**Pre-release.** Pub.dev consumers with caret ranges (`^0.3.0` or `^0.4.0`) -will not auto-pick this up; pin explicitly (`dart_skills_lint: 0.4.0-dev.1`) -to try the preview. - -### Distribution - -- Native binaries are now published to GitHub Releases for macOS arm64, - macOS x64, Linux x64, and Linux arm64. Users without the Dart SDK on - PATH can install the linter via `install.sh` or a direct `curl -L` of - the release tarball — no `dart pub global activate` required. -- New `tool/dart_skills_lint/scripts/install.sh`: detects OS/arch, - downloads the matching tarball from the latest release, verifies its - SHA256 against the release's `SHA256SUMS`, and installs the binary to - `INSTALL_DIR` (default `/usr/local/bin`, with a sudo fallback). - `REPO`, `VERSION`, and `INSTALL_DIR` are env-configurable. -- New `.github/workflows/dart_skills_lint_release.yaml`: tag-triggered - workflow that builds the four-target matrix with `dart compile exe`, - packages each as `dart_skills_lint-.tar.gz`, generates a - `SHA256SUMS` aggregate, and cuts a GitHub Release whose body is - extracted from this CHANGELOG entry. -- The pub.dev install paths (`dart pub global activate dart_skills_lint` - and `dev_dependencies:`) are unchanged. Native binaries are a parallel - channel, not a replacement. -- macOS binaries in this preview are **not yet code-signed**. Users will - see a Gatekeeper warning on first launch. Workaround: - `xattr -d com.apple.quarantine $(which dart_skills_lint)`. Signing - lands once the org Developer ID Application cert is approved. -- Homebrew formula is **deferred**: `dart_skills_lint` is being moved to - a dedicated repo (location TBD), and a `brew tap` would force every - early adopter through a re-tap on migration. The formula ships from - the new repo once it exists. +- **Pre-release.** Pin explicitly (`dart_skills_lint: 0.4.0-dev.1`) to + try the preview — caret ranges will not auto-pick this up. +- Native binaries for macOS arm64, macOS x64, Linux x64, and Linux + arm64 are now published to GitHub Releases. Install without the + Dart SDK via `curl -fsSL .../install.sh | bash`, or download the + tarball directly and verify its SHA256. +- macOS preview binaries are not yet code-signed; clear the + quarantine flag with + `xattr -d com.apple.quarantine $(which dart_skills_lint)` on + first launch. +- The `dart pub global activate dart_skills_lint` and + `dev_dependencies:` install paths are unchanged. ## 0.3.1 diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md index 8e6df89..37b6004 100644 --- a/tool/dart_skills_lint/README.md +++ b/tool/dart_skills_lint/README.md @@ -34,7 +34,38 @@ matches your environment. > dedicated repository. Until then, the install paths below cover all > supported platforms. -### 1. `install.sh` — Linux + macOS, no Dart required +### 1. Dart developers — pub.dev + +If you already have the Dart SDK installed, the standard pub.dev paths +still work and are unchanged. + +#### As a project dev_dependency + +Add to your `pubspec.yaml`: +```yaml +dev_dependencies: + dart_skills_lint: ^0.3.0 +``` + +To opt into the preview track, pin to the exact version: +```yaml +dev_dependencies: + dart_skills_lint: 0.4.0-dev.1 +``` + +Then: +```bash +dart pub get +``` + +#### Globally activated + +For multiple projects without per-project pubspec entries: +```bash +dart pub global activate dart_skills_lint +``` + +### 2. `install.sh` — Linux + macOS, no Dart required The recommended path for CI runners and laptops without the Dart SDK on PATH. Downloads the matching prebuilt binary from the latest GitHub @@ -62,7 +93,7 @@ xattr -d com.apple.quarantine "$(which dart_skills_lint)" This step goes away once notarized builds ship. -### 2. Direct download — Linux + macOS, no install script +### 3. Direct download — Linux + macOS, no install script For environments where piping a script to `bash` isn't acceptable. Grab the tarball for your platform from @@ -82,37 +113,6 @@ sudo install -m 0755 "dart_skills_lint-${TARGET}" /usr/local/bin/dart_skills_lin On macOS, replace `sha256sum -c -` with `shasum -a 256 -c -`. -### 3. Dart developers — pub.dev - -If you already have the Dart SDK installed, the standard pub.dev paths -still work and are unchanged. - -#### As a project dev_dependency - -Add to your `pubspec.yaml`: -```yaml -dev_dependencies: - dart_skills_lint: ^0.3.0 -``` - -To opt into the preview track, pin to the exact version: -```yaml -dev_dependencies: - dart_skills_lint: 0.4.0-dev.1 -``` - -Then: -```bash -dart pub get -``` - -#### Globally activated - -For multiple projects without per-project pubspec entries: -```bash -dart pub global activate dart_skills_lint -``` - ## Usage `dart_skills_lint` runs as a command-line tool, configured by flags or by diff --git a/tool/dart_skills_lint/scripts/install.sh b/tool/dart_skills_lint/scripts/install.sh index 9fc858d..9d2b308 100755 --- a/tool/dart_skills_lint/scripts/install.sh +++ b/tool/dart_skills_lint/scripts/install.sh @@ -24,22 +24,28 @@ err() { echo "install.sh: error: $*" >&2; exit 1; } info() { echo "install.sh: $*"; } # --- Detect platform --------------------------------------------------------- +# Single source of truth: every "Supported: ..." message and the final +# platform check derive from this list, so adding a build target only +# requires touching one constant. +SUPPORTED_TARGETS="macos-arm64 macos-x64 linux-x64 linux-arm64" +err_unsupported() { err "$1. Supported platforms: ${SUPPORTED_TARGETS// /, }."; } + case "$(uname -s)" in Darwin) os="macos" ;; Linux) os="linux" ;; - *) err "unsupported OS '$(uname -s)'. Supported: macOS, Linux." ;; + *) err_unsupported "unsupported OS '$(uname -s)'" ;; esac case "$(uname -m)" in arm64|aarch64) arch="arm64" ;; x86_64|amd64) arch="x64" ;; - *) err "unsupported architecture '$(uname -m)'. Supported: arm64, aarch64, x86_64, amd64." ;; + *) err_unsupported "unsupported architecture '$(uname -m)'" ;; esac target="${os}-${arch}" -case "$target" in - macos-arm64|macos-x64|linux-x64|linux-arm64) ;; - *) err "no published binary for platform '${target}'. Available: macos-arm64, macos-x64, linux-x64, linux-arm64." ;; +case " $SUPPORTED_TARGETS " in + *" $target "*) ;; + *) err_unsupported "no published binary for platform '${target}'" ;; esac # --- Required tools --------------------------------------------------------- From 3130b34ec684ec3cca0c7a5fb58f6097e44bd2f8 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 11:26:16 -0400 Subject: [PATCH 06/11] Refactor install.sh tests to be robust, add Linux tests and missing error coverage --- tool/dart_skills_lint/scripts/install.sh | 10 +- .../test/install_script_test.dart | 462 ++++++++++++++++++ 2 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 tool/dart_skills_lint/test/install_script_test.dart diff --git a/tool/dart_skills_lint/scripts/install.sh b/tool/dart_skills_lint/scripts/install.sh index 9d2b308..d550e9e 100755 --- a/tool/dart_skills_lint/scripts/install.sh +++ b/tool/dart_skills_lint/scripts/install.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash # +# Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +# for details. All rights reserved. Use of this source code is governed by a +# BSD-style license that can be found in the LICENSE file. +# # install.sh — Install the dart_skills_lint native binary. # # Usage (default repo + latest version): @@ -52,13 +56,14 @@ esac require() { command -v "$1" >/dev/null 2>&1 || err "required tool '$1' not found on PATH."; } require curl require tar +require awk if command -v sha256sum >/dev/null 2>&1; then shasum_cmd() { sha256sum "$@"; } elif command -v shasum >/dev/null 2>&1; then shasum_cmd() { shasum -a 256 "$@"; } else - err "neither 'sha256sum' nor 'shasum' is available. Install one to verify the binary." + err "required tools 'sha256sum' or 'shasum' not found on PATH. Install one to verify the binary." fi # --- Resolve URLs ---------------------------------------------------------- @@ -74,7 +79,8 @@ sums_url="${base_url}/SHA256SUMS" # --- Download into a tempdir, cleaned up on exit ----------------------------- tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dart-skills-lint-install.XXXXXX")" -trap 'rm -rf "$tmpdir"' EXIT INT TERM +# Guard trap to prevent running rm -rf on empty/unbound tmpdir if trap triggers prematurely. +trap '[ -n "${tmpdir:-}" ] && rm -rf "$tmpdir"' EXIT INT TERM info "downloading ${archive} from ${REPO} (${VERSION})" curl -fsSL --retry 3 -o "${tmpdir}/${archive}" "$archive_url" \ diff --git a/tool/dart_skills_lint/test/install_script_test.dart b/tool/dart_skills_lint/test/install_script_test.dart new file mode 100644 index 0000000..1449bf8 --- /dev/null +++ b/tool/dart_skills_lint/test/install_script_test.dart @@ -0,0 +1,462 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_process/test_process.dart'; + +/// A dummy SHA256 checksum that represents an invalid/corrupted release file hash. +const _corruptedHash = '0000000000000000000000000000000000000000000000000000000000000000'; + +/// A mock implementation of the `curl` command line utility. +/// +/// Simulates downloading a release artifact by copying the source file from +/// `MOCK_RELEASE_DIR` to the target output path specified by `-o`. +/// +/// It ignores other standard `curl` flags. +const _mockCurlScript = r''' +#!/bin/bash +set -eu +outfile="" +url="" +while [ $# -gt 0 ]; do + case "$1" in + -o) outfile="$2"; shift ;; + --retry) shift ;; + -*) ;; + *) url="$1" ;; + esac + shift +done +filename="$(basename "$url")" +src_file="${MOCK_RELEASE_DIR}/$filename" +if [ -f "$src_file" ]; then + cp "$src_file" "$outfile" +else + echo "mock curl: error: source file $src_file not found ($url)" >&2 + exit 1 +fi +'''; + +/// A mock implementation of the `uname` command line utility. +/// +/// Outputs the mocked OS (`MOCK_UNAME_S`) or CPU architecture (`MOCK_UNAME_M`) +/// depending on whether it is executed with `-s` or `-m` flags. +const _mockUnameScript = r''' +#!/bin/bash +if [ "$1" = "-s" ]; then + echo "${MOCK_UNAME_S:-Darwin}" +elif [ "$1" = "-m" ]; then + echo "${MOCK_UNAME_M:-arm64}" +fi +'''; + +void main() { + group('install.sh integration', () { + late Directory tempDir; + late Directory mockBinDir; + late Directory mockReleaseDir; + late Directory installDir; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('install_sh_test.'); + mockBinDir = await Directory(p.join(tempDir.path, 'bin')).create(); + mockReleaseDir = await Directory(p.join(tempDir.path, 'mock_release')).create(); + installDir = await Directory(p.join(tempDir.path, 'install')).create(); + + // Write mock uname script + final unameFile = File(p.join(mockBinDir.path, 'uname')); + await unameFile.writeAsString(_mockUnameScript); + final ProcessResult chmodUnameResult = await Process.run('chmod', ['+x', unameFile.path]); + expect( + chmodUnameResult.exitCode, + 0, + reason: 'chmod failed for uname mock: ${chmodUnameResult.stderr}', + ); + + // Write mock curl script + final curlFile = File(p.join(mockBinDir.path, 'curl')); + await curlFile.writeAsString(_mockCurlScript); + final ProcessResult chmodCurlResult = await Process.run('chmod', ['+x', curlFile.path]); + expect( + chmodCurlResult.exitCode, + 0, + reason: 'chmod failed for curl mock: ${chmodCurlResult.stderr}', + ); + }); + + tearDown(() async { + if (tempDir.existsSync()) { + await tempDir.delete(recursive: true); + } + }); + + /// Simulates a packaged GitHub release asset by writing a dummy binary, + /// compressing it to a `.tar.gz` archive in the mock release directory, + /// and generating the corresponding `SHA256SUMS` checksum file. + /// + /// If [shouldCorruptHash] is true, the `SHA256SUMS` file will be written with + /// an invalid hash to test checksum verification failure paths. + Future createMockRelease({ + required String os, + required String arch, + required String version, + required String binaryContent, + bool shouldCorruptHash = false, + }) async { + final target = '$os-$arch'; + final binaryName = 'dart_skills_lint-$target'; + final archiveName = 'dart_skills_lint-$target.tar.gz'; + + // Create dummy binary file + final dummyBin = File(p.join(tempDir.path, binaryName)); + await dummyBin.writeAsString(binaryContent); + final ProcessResult chmodBinResult = await Process.run('chmod', ['+x', dummyBin.path]); + expect( + chmodBinResult.exitCode, + 0, + reason: 'chmod failed for dummy binary: ${chmodBinResult.stderr}', + ); + + // Package it into tar.gz + final ProcessResult tarResult = await Process.run('tar', [ + '-czf', + p.join(mockReleaseDir.path, archiveName), + '-C', + tempDir.path, + binaryName, + ]); + expect(tarResult.exitCode, 0, reason: 'tar packaging failed: ${tarResult.stderr}'); + + // Get SHA256 sum + var hash = ''; + // TODO(reidbaker): Re-add CertUtil checksum verification for Windows hosts. https://github.com/flutter/skills/issues/164 + final ProcessResult shaProcess = await Process.run('shasum', [ + '-a', + '256', + p.join(mockReleaseDir.path, archiveName), + ]); + if (shaProcess.exitCode == 0) { + hash = shaProcess.stdout.toString().trim().split(' ')[0]; + } else { + final ProcessResult sha256Process = await Process.run('sha256sum', [ + p.join(mockReleaseDir.path, archiveName), + ]); + if (sha256Process.exitCode == 0) { + hash = sha256Process.stdout.toString().trim().split(' ')[0]; + } + } + + if (hash.isEmpty) { + throw StateError('Could not calculate SHA256 hash using shasum or sha256sum.'); + } + + final String finalHash = shouldCorruptHash ? _corruptedHash : hash; + + final sha256sums = File(p.join(mockReleaseDir.path, 'SHA256SUMS')); + await sha256sums.writeAsString('$finalHash $archiveName\n'); + } + + Future runInstallScriptTest({ + required String os, + required String arch, + required String mockUnameS, + required String mockUnameM, + required bool simulateLaunchFailure, + required int expectedExitCode, + required bool expectInstalled, + }) async { + const version = '0.4.0-test'; + final binaryContent = simulateLaunchFailure + ? '#!/usr/bin/env bash\nexit 1\n' + : '#!/usr/bin/env bash\necho "mock-cli-help"\n'; + + await createMockRelease(os: os, arch: arch, version: version, binaryContent: binaryContent); + + // TODO(reidbaker): Use Windows path separator (;) when running on Windows hosts. https://github.com/flutter/skills/issues/164 + final newPath = '${mockBinDir.path}:${Platform.environment['PATH']}'; + final String packageRoot = _getPackageRoot(); + final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); + + final TestProcess process = await TestProcess.start( + 'bash', + [scriptPath], + environment: { + 'PATH': newPath, + 'MOCK_UNAME_S': mockUnameS, + 'MOCK_UNAME_M': mockUnameM, + 'MOCK_RELEASE_DIR': mockReleaseDir.path, + 'INSTALL_DIR': installDir.path, + 'VERSION': version, + }, + ); + + await process.shouldExit(expectedExitCode); + + final installedFile = File(p.join(installDir.path, 'dart_skills_lint')); + expect(installedFile.existsSync(), equals(expectInstalled)); + + if (expectInstalled && expectedExitCode == 0) { + if (simulateLaunchFailure) { + final List stdout = await process.stdout.rest.toList(); + expect( + stdout.any((line) => line.contains('launch check failed — likely Gatekeeper')), + isTrue, + ); + } else { + final ProcessResult runResult = await Process.run(installedFile.path, ['--help']); + expect(runResult.stdout.toString().trim(), equals('mock-cli-help')); + } + } else if (expectedExitCode == 1 && simulateLaunchFailure) { + final List stderr = await process.stderr.rest.toList(); + expect(stderr.any((line) => line.contains('failed to launch')), isTrue); + } + } + + test('successful installation on macos-arm64', () async { + await runInstallScriptTest( + os: 'macos', + arch: 'arm64', + mockUnameS: 'Darwin', + mockUnameM: 'arm64', + simulateLaunchFailure: false, + expectedExitCode: 0, + expectInstalled: true, + ); + }); + + test('successful installation on linux-x64', () async { + await runInstallScriptTest( + os: 'linux', + arch: 'x64', + mockUnameS: 'Linux', + mockUnameM: 'x86_64', + simulateLaunchFailure: false, + expectedExitCode: 0, + expectInstalled: true, + ); + }); + + test('fails on linux if installed binary fails launch check', () async { + await runInstallScriptTest( + os: 'linux', + arch: 'x64', + mockUnameS: 'Linux', + mockUnameM: 'x86_64', + simulateLaunchFailure: true, + expectedExitCode: 1, + expectInstalled: true, + ); + }); + + test('succeeds on macos even if installed binary fails launch check', () async { + await runInstallScriptTest( + os: 'macos', + arch: 'arm64', + mockUnameS: 'Darwin', + mockUnameM: 'arm64', + simulateLaunchFailure: true, + expectedExitCode: 0, + expectInstalled: true, + ); + }); + + test('fails if checksum mismatch', () async { + const version = '0.4.0-test'; + await createMockRelease( + os: 'macos', + arch: 'arm64', + version: version, + binaryContent: 'dummy', + shouldCorruptHash: true, + ); + + // TODO(reidbaker): Use Windows path separator (;) when running on Windows hosts. https://github.com/flutter/skills/issues/164 + final newPath = '${mockBinDir.path}:${Platform.environment['PATH']}'; + final String packageRoot = _getPackageRoot(); + final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); + + final TestProcess process = await TestProcess.start( + 'bash', + [scriptPath], + environment: { + 'PATH': newPath, + 'MOCK_UNAME_S': 'Darwin', + 'MOCK_UNAME_M': 'arm64', + 'MOCK_RELEASE_DIR': mockReleaseDir.path, + 'INSTALL_DIR': installDir.path, + 'VERSION': version, + }, + ); + + final List stderr = await process.stderr.rest.toList(); + expect(stderr.any((line) => line.contains('SHA256 mismatch')), isTrue); + await process.shouldExit(1); + }); + + group('missing required tools', () { + final requiredTools = ['curl', 'tar', 'awk']; + + for (var i = 0; i < requiredTools.length; i++) { + final String missingTool = requiredTools[i]; + + test('fails if required tool $missingTool is missing', () async { + // Create a directory containing mock uname and all required tools before this one + final Directory testBinDir = await Directory( + p.join(tempDir.path, 'bin_$missingTool'), + ).create(); + + // Always copy mock uname + final mockUnameFile = File(p.join(mockBinDir.path, 'uname')); + await mockUnameFile.copy(p.join(testBinDir.path, 'uname')); + final ProcessResult chmodUnameResult = await Process.run('chmod', [ + '+x', + p.join(testBinDir.path, 'uname'), + ]); + expect(chmodUnameResult.exitCode, 0); + + // Copy all mock tools prior to this one in the dependency order + for (var j = 0; j < i; j++) { + final String toolToCopy = requiredTools[j]; + if (toolToCopy == 'curl') { + final mockCurlFile = File(p.join(mockBinDir.path, 'curl')); + await mockCurlFile.copy(p.join(testBinDir.path, 'curl')); + final ProcessResult chmodCurlResult = await Process.run('chmod', [ + '+x', + p.join(testBinDir.path, 'curl'), + ]); + expect(chmodCurlResult.exitCode, 0); + } else { + // Write a dummy script for other tools (like tar) so they pass command -v check + final dummyFile = File(p.join(testBinDir.path, toolToCopy)); + await dummyFile.writeAsString('#!/bin/bash\nexit 0\n'); + final ProcessResult chmodDummyResult = await Process.run('chmod', [ + '+x', + dummyFile.path, + ]); + expect(chmodDummyResult.exitCode, 0); + } + } + + final String packageRoot = _getPackageRoot(); + final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); + + final TestProcess process = await TestProcess.start( + '/bin/bash', + [scriptPath], + environment: { + 'PATH': testBinDir.path, + 'MOCK_UNAME_S': 'Darwin', + 'MOCK_UNAME_M': 'arm64', + 'INSTALL_DIR': installDir.path, + }, + ); + + final List stderr = await process.stderr.rest.toList(); + expect( + stderr.any((line) => line.contains("required tool '$missingTool' not found on PATH")), + isTrue, + ); + await process.shouldExit(1); + }); + } + + test('fails if both sha256sum and shasum are missing', () async { + final Directory testBinDir = await Directory(p.join(tempDir.path, 'bin_no_hash')).create(); + + // Copy mock uname, curl, and dummy tar, awk + final mockUnameFile = File(p.join(mockBinDir.path, 'uname')); + await mockUnameFile.copy(p.join(testBinDir.path, 'uname')); + final ProcessResult chmodUname = await Process.run('chmod', [ + '+x', + p.join(testBinDir.path, 'uname'), + ]); + expect(chmodUname.exitCode, 0); + + final mockCurlFile = File(p.join(mockBinDir.path, 'curl')); + await mockCurlFile.copy(p.join(testBinDir.path, 'curl')); + final ProcessResult chmodCurl = await Process.run('chmod', [ + '+x', + p.join(testBinDir.path, 'curl'), + ]); + expect(chmodCurl.exitCode, 0); + + final dummyTar = File(p.join(testBinDir.path, 'tar')); + await dummyTar.writeAsString('#!/bin/bash\nexit 0\n'); + final ProcessResult chmodTar = await Process.run('chmod', ['+x', dummyTar.path]); + expect(chmodTar.exitCode, 0); + + final dummyAwk = File(p.join(testBinDir.path, 'awk')); + await dummyAwk.writeAsString('#!/bin/bash\nexit 0\n'); + final ProcessResult chmodAwk = await Process.run('chmod', ['+x', dummyAwk.path]); + expect(chmodAwk.exitCode, 0); + + final String packageRoot = _getPackageRoot(); + final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); + + final TestProcess process = await TestProcess.start( + '/bin/bash', + [scriptPath], + environment: { + 'PATH': testBinDir.path, + 'MOCK_UNAME_S': 'Darwin', + 'MOCK_UNAME_M': 'arm64', + 'INSTALL_DIR': installDir.path, + }, + ); + + final List stderr = await process.stderr.rest.toList(); + expect( + stderr.any( + (line) => + line.contains('sha256sum') && line.contains('shasum') && line.contains('not found'), + ), + isTrue, + ); + await process.shouldExit(1); + }); + }); + + test('fails on unsupported architecture', () async { + // TODO(reidbaker): Use Windows path separator (;) when running on Windows hosts. https://github.com/flutter/skills/issues/164 + final newPath = '${mockBinDir.path}:${Platform.environment['PATH']}'; + final String packageRoot = _getPackageRoot(); + final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); + + final TestProcess process = await TestProcess.start( + 'bash', + [scriptPath], + environment: { + 'PATH': newPath, + 'MOCK_UNAME_S': 'Linux', + 'MOCK_UNAME_M': 'i386', + 'INSTALL_DIR': installDir.path, + }, + ); + + final List stderr = await process.stderr.rest.toList(); + expect(stderr.any((line) => line.contains('unsupported architecture')), isTrue); + await process.shouldExit(1); + }); + // TODO(reidbaker): Support running install.sh tests on Windows hosts. https://github.com/flutter/skills/issues/164 + }, skip: Platform.isWindows ? 'install.sh is not supported on Windows' : null); +} + +String _getPackageRoot() { + Directory dir = Directory.current; + while (dir.path != '/' && dir.path.isNotEmpty) { + final pubspec = File(p.join(dir.path, 'pubspec.yaml')); + if (pubspec.existsSync() && pubspec.readAsStringSync().contains('name: dart_skills_lint')) { + return dir.path; + } + dir = dir.parent; + } + // Fallback to searching subdirectories + final subdir = Directory(p.join(Directory.current.path, 'tool', 'dart_skills_lint')); + if (subdir.existsSync()) { + return subdir.path; + } + return Directory.current.path; +} From c82236fb71397493f7bda2df785128c2dbcf401f Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 11:28:43 -0400 Subject: [PATCH 07/11] Fix custom linter warnings and format test file --- tool/dart_skills_lint/test/install_script_test.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tool/dart_skills_lint/test/install_script_test.dart b/tool/dart_skills_lint/test/install_script_test.dart index 1449bf8..5da38bd 100644 --- a/tool/dart_skills_lint/test/install_script_test.dart +++ b/tool/dart_skills_lint/test/install_script_test.dart @@ -15,7 +15,7 @@ const _corruptedHash = '00000000000000000000000000000000000000000000000000000000 /// Simulates downloading a release artifact by copying the source file from /// `MOCK_RELEASE_DIR` to the target output path specified by `-o`. /// -/// It ignores other standard `curl` flags. +/// It ignores other standard `curl` flags. const _mockCurlScript = r''' #!/bin/bash set -eu @@ -102,7 +102,6 @@ void main() { Future createMockRelease({ required String os, required String arch, - required String version, required String binaryContent, bool shouldCorruptHash = false, }) async { @@ -173,7 +172,7 @@ void main() { ? '#!/usr/bin/env bash\nexit 1\n' : '#!/usr/bin/env bash\necho "mock-cli-help"\n'; - await createMockRelease(os: os, arch: arch, version: version, binaryContent: binaryContent); + await createMockRelease(os: os, arch: arch, binaryContent: binaryContent); // TODO(reidbaker): Use Windows path separator (;) when running on Windows hosts. https://github.com/flutter/skills/issues/164 final newPath = '${mockBinDir.path}:${Platform.environment['PATH']}'; @@ -268,7 +267,6 @@ void main() { await createMockRelease( os: 'macos', arch: 'arm64', - version: version, binaryContent: 'dummy', shouldCorruptHash: true, ); @@ -445,7 +443,8 @@ void main() { } String _getPackageRoot() { - Directory dir = Directory.current; + final String currentPath = Directory.current.path; + Directory dir = Directory(currentPath); while (dir.path != '/' && dir.path.isNotEmpty) { final pubspec = File(p.join(dir.path, 'pubspec.yaml')); if (pubspec.existsSync() && pubspec.readAsStringSync().contains('name: dart_skills_lint')) { @@ -454,9 +453,9 @@ String _getPackageRoot() { dir = dir.parent; } // Fallback to searching subdirectories - final subdir = Directory(p.join(Directory.current.path, 'tool', 'dart_skills_lint')); + final subdir = Directory(p.join(currentPath, 'tool', 'dart_skills_lint')); if (subdir.existsSync()) { return subdir.path; } - return Directory.current.path; + return currentPath; } From 20fbf39468c0692d2e27339aa537047aeb73d662 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 11:31:32 -0400 Subject: [PATCH 08/11] Fix omit_obvious_local_variable_types analyzer warning in test --- tool/dart_skills_lint/test/install_script_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/dart_skills_lint/test/install_script_test.dart b/tool/dart_skills_lint/test/install_script_test.dart index 5da38bd..30b07f0 100644 --- a/tool/dart_skills_lint/test/install_script_test.dart +++ b/tool/dart_skills_lint/test/install_script_test.dart @@ -444,7 +444,7 @@ void main() { String _getPackageRoot() { final String currentPath = Directory.current.path; - Directory dir = Directory(currentPath); + var dir = Directory(currentPath); while (dir.path != '/' && dir.path.isNotEmpty) { final pubspec = File(p.join(dir.path, 'pubspec.yaml')); if (pubspec.existsSync() && pubspec.readAsStringSync().contains('name: dart_skills_lint')) { From ef8475134afc175937e73c03ab8734dcbeb1b429 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 13:13:50 -0400 Subject: [PATCH 09/11] Pin softprops/action-gh-release to commit SHA to resolve review feedback --- .github/workflows/dart_skills_lint_release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dart_skills_lint_release.yaml b/.github/workflows/dart_skills_lint_release.yaml index e0f7e2a..327511f 100644 --- a/.github/workflows/dart_skills_lint_release.yaml +++ b/.github/workflows/dart_skills_lint_release.yaml @@ -142,7 +142,7 @@ jobs: VERSION: ${{ steps.meta.outputs.version }} - name: Create GitHub Release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda with: tag_name: ${{ github.ref_name }} name: dart_skills_lint v${{ steps.meta.outputs.version }} From 83d4c2d52381f0bf5595db2e676a33c2663ae70a Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 13:29:50 -0400 Subject: [PATCH 10/11] Pin VeryGoodOpenSource/very_good_coverage to commit SHA --- .github/workflows/dart_skills_lint_workflow.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dart_skills_lint_workflow.yaml b/.github/workflows/dart_skills_lint_workflow.yaml index 4cf45f9..9c91c78 100644 --- a/.github/workflows/dart_skills_lint_workflow.yaml +++ b/.github/workflows/dart_skills_lint_workflow.yaml @@ -61,7 +61,7 @@ jobs: # Action steps do not inherit defaults.run.working-directory, so this # path is relative to the repository root. - name: Enforce coverage threshold - uses: VeryGoodOpenSource/very_good_coverage@v3 + uses: VeryGoodOpenSource/very_good_coverage@c953fca3e24a915e111cc6f55f03f756dcb3964c with: path: tool/dart_skills_lint/coverage/lcov.info min_coverage: 73 From e578b304d347013e7f0ad84194f038d339b88d81 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 13:30:21 -0400 Subject: [PATCH 11/11] Add tag mapping comments to pinned action commit SHAs --- .github/workflows/dart_skills_lint_release.yaml | 2 +- .github/workflows/dart_skills_lint_workflow.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dart_skills_lint_release.yaml b/.github/workflows/dart_skills_lint_release.yaml index 327511f..38befa8 100644 --- a/.github/workflows/dart_skills_lint_release.yaml +++ b/.github/workflows/dart_skills_lint_release.yaml @@ -142,7 +142,7 @@ jobs: VERSION: ${{ steps.meta.outputs.version }} - name: Create GitHub Release - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: tag_name: ${{ github.ref_name }} name: dart_skills_lint v${{ steps.meta.outputs.version }} diff --git a/.github/workflows/dart_skills_lint_workflow.yaml b/.github/workflows/dart_skills_lint_workflow.yaml index 9c91c78..63642f9 100644 --- a/.github/workflows/dart_skills_lint_workflow.yaml +++ b/.github/workflows/dart_skills_lint_workflow.yaml @@ -61,7 +61,7 @@ jobs: # Action steps do not inherit defaults.run.working-directory, so this # path is relative to the repository root. - name: Enforce coverage threshold - uses: VeryGoodOpenSource/very_good_coverage@c953fca3e24a915e111cc6f55f03f756dcb3964c + uses: VeryGoodOpenSource/very_good_coverage@c953fca3e24a915e111cc6f55f03f756dcb3964c # v3 with: path: tool/dart_skills_lint/coverage/lcov.info min_coverage: 73