diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 993af2b..48a1b79 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -64,6 +64,27 @@ jobs: - run: ./install.ps1 -DryRun shell: pwsh + pypi-wheel: + name: PyPI wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + version: "~> v2" + args: build --snapshot --clean --single-target + - run: ./packaging/pypi/build-wheels.sh + - run: uvx twine check dist/pypi/*.whl + # The wheel ships no Python: this proves the binary lands on PATH. + - run: uvx --from ./dist/pypi/*.whl flagsmith --version + cross-compile: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b1949a..6377b37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 with: @@ -55,6 +56,14 @@ jobs: with: subject-checksums: ./dist/digests.txt + - name: Build PyPI wheels + run: ./packaging/pypi/build-wheels.sh + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pypi-wheels + path: dist/pypi/*.whl + if-no-files-found: error + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 24 # npm >= 11.5.1 required for OIDC trusted publishing @@ -78,12 +87,21 @@ jobs: --keywords flagsmith feature-flags cli --files README.md LICENSE - # During public beta the newest beta is what people - # should land on, so clear it. - - if: contains(github.ref_name, '-beta') - run: gh release edit "$GITHUB_REF_NAME" --prerelease=false --latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + pypi: + name: Publish to PyPI + needs: goreleaser + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # PyPI trusted publishing + PEP 740 attestations + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pypi-wheels + path: dist + - uses: pypa/gh-action-pypi-publish@a892a5a61159132606e93a2fa6f4358831b04d26 # v1.14.2 + with: + packages-dir: dist install-script: name: install.sh (${{ matrix.os }}) diff --git a/.gitignore b/.gitignore index a94b49e..45bcda3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /flagsmith /dist/ *.test +# Staged by packaging/pypi/build-wheels.sh +/packaging/pypi/bin/ +/packaging/pypi/README.md +/packaging/pypi/LICENSE diff --git a/README.md b/README.md index 43335dc..a26cb89 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,13 @@ curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/instal To pin the installer itself, fetch it at a commit you trust: `raw.githubusercontent.com/Flagsmith/flagsmith-cli//install.sh`. +With [uv](https://docs.astral.sh/uv/): + +```sh +uv tool install flagsmith-cli # installs the `flagsmith` command +uvx --from flagsmith-cli flagsmith --help +``` + Alternatively, `go install github.com/Flagsmith/flagsmith-cli/v2@v2.0.0-beta.3` (installs as `flagsmith-cli`), or grab an archive from [Releases](https://github.com/Flagsmith/flagsmith-cli/releases). On Windows: diff --git a/packaging/pypi/build-wheels.sh b/packaging/pypi/build-wheels.sh new file mode 100755 index 0000000..253dd13 --- /dev/null +++ b/packaging/pypi/build-wheels.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Build PyPI wheels around the binaries from GoReleaser. +# +# hatchling builds each wheel (see pyproject.toml) and `wheel tags` stamps the +# platform tag on it, so there is no packaging code of our own to maintain. +# +# Reads dist/artifacts.json + dist/metadata.json, writes wheels to dist/pypi/. +# +# packaging/pypi/build-wheels.sh [dist-dir] +set -euo pipefail + +dist=${1:-dist} +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo=$(cd "$here/../.." && pwd) +out=$repo/$dist/pypi + +# CGO is disabled, so the Linux binaries are static and run on musl too. +# The macOS tags are a minimum, kept at https://go.dev/wiki/MinimumRequirements +platform_tag() { + case "$1/$2" in + darwin/amd64) echo macosx_12_0_x86_64 ;; + darwin/arm64) echo macosx_12_0_arm64 ;; + linux/amd64) echo manylinux2014_x86_64.musllinux_1_1_x86_64 ;; + linux/arm64) echo manylinux2014_aarch64.musllinux_1_1_aarch64 ;; + windows/amd64) echo win_amd64 ;; + windows/arm64) echo win_arm64 ;; + *) return 1 ;; + esac +} + +version=$(jq -re '.tag | ltrimstr("v")' "$dist/metadata.json") +rm -rf "$out" "${here:?}/bin" +mkdir -p "$out" "$here/bin" +cp "$repo/README.md" "$repo/LICENSE" "$here/" + +built=0 +while read -r goos goarch path; do + tag=$(platform_tag "$goos" "$goarch") || { + echo "skipping $goos/$goarch: no wheel platform tag" >&2 + continue + } + script=flagsmith + [ "$goos" = windows ] && script=flagsmith.exe + rm -f "$here"/bin/flagsmith* + install -m 755 "$path" "$here/bin/$script" + + FLAGSMITH_CLI_VERSION=$version uv build --quiet --wheel "$here" --out-dir "$out" + uvx --from wheel wheel tags --python-tag py3 --abi-tag none \ + --platform-tag "$tag" --remove "$out/flagsmith_cli-"*"-py3-none-any.whl" + echo "$goos/$goarch -> $tag" + built=$((built + 1)) +done < <(jq -re '.[] | select(.type == "Binary") | [.goos, .goarch, .path] | @tsv' "$dist/artifacts.json") + +rm -rf "${here:?}/bin" "$here/README.md" "$here/LICENSE" +[ "$built" -gt 0 ] || { + echo "no binaries found in $dist/artifacts.json" >&2 + exit 1 +} diff --git a/packaging/pypi/pyproject.toml b/packaging/pypi/pyproject.toml new file mode 100644 index 0000000..e12f0e7 --- /dev/null +++ b/packaging/pypi/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "flagsmith-cli" +dynamic = ["version"] +description = "The Flagsmith command-line interface" +readme = "README.md" # staged by build-wheels.sh +license = "MIT" +license-files = ["LICENSE"] # staged by build-wheels.sh +requires-python = ">=3.8" +keywords = ["cli", "feature-flags", "flagsmith"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Programming Language :: Go", + "Topic :: Software Development", +] + +[project.urls] +Homepage = "https://github.com/Flagsmith/flagsmith-cli" +Source = "https://github.com/Flagsmith/flagsmith-cli" +Issues = "https://github.com/Flagsmith/flagsmith-cli/issues" + +# The Go tag, minus its "v"; hatchling normalises it to PEP 440 +# (2.0.0-beta.3 -> 2.0.0b3). +[tool.hatch.version] +source = "env" +variable = "FLAGSMITH_CLI_VERSION" + +# The binary ships in the wheel's .data/scripts/ directory, which every +# installer drops straight onto PATH: `uv tool install flagsmith-cli` (and pip, +# and pipx) hand you a working `flagsmith` with no Python shim in the way. +# Only one of these two exists per build; hatchling skips the other. +[tool.hatch.build.targets.wheel] +bypass-selection = true # no Python package to ship +shared-scripts = { "bin/flagsmith" = "flagsmith", "bin/flagsmith.exe" = "flagsmith.exe" }