diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fff4cbc..517231c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,16 @@ permissions: jobs: test: + name: test (${{ matrix.check }}) runs-on: ubuntu-latest strategy: matrix: - go-version: ["1.27.0", "1.27.1"] + include: + - go-version: "1.27.0" + check: "1.27.0" + # Preserve the required status name while testing the patched compiler. + - go-version: "1.27.2" + check: "1.27.1" env: GOTOOLCHAIN: local steps: @@ -44,7 +50,7 @@ jobs: args: --timeout=3m macos-release: - runs-on: macos-15 + runs-on: xcode-27 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -63,3 +69,17 @@ jobs: run: | goreleaser check goreleaser build --snapshot --clean + - name: Smoke-test both macOS architectures + run: | + arch -x86_64 /usr/bin/true || sudo softwareupdate --install-rosetta --agree-to-license + arch -x86_64 /usr/bin/true + for target in amd64 arm64; do + for binary in dist/ordercli_darwin_"${target}"*/ordercli; do + test -x "$binary" + if [ "$target" = amd64 ]; then + arch -x86_64 "$binary" --version + else + "$binary" --version + fi + done + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f5f3f4..e1af51f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,10 @@ name: release on: workflow_dispatch: inputs: + dry_run: + description: Build, sign and verify without creating tags or releases + type: boolean + default: true version: description: SemVer to release (for example 0.2.1) required: true @@ -30,8 +34,9 @@ jobs: contents: write pull-requests: write statuses: read - uses: openclaw/release-workflows/.github/workflows/release-go-cli.yml@f613cbfed2b043159c850c353e7facb8c89833b0 # v1.9.0 + uses: openclaw/release-workflows/.github/workflows/release-go-cli.yml@fc49859568b3781af7663bee48c003fd987dc08b # v1.11.8 with: + dry-run: ${{ inputs.dry_run }} version: ${{ inputs.version }} repository-type: personal build-runner: macos diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd915f..1210072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Move macOS CI and release verification to Xcode 27, preserving macOS 13 and verifying Intel CLI execution under Rosetta; add non-publishing release dry runs. +- Build with patched Go 1.27.2 while retaining the Go 1.27.0 source minimum. + ## 0.2.1 (2026-09-14) **Highlights:** macOS downloads are signed and notarized. diff --git a/Dockerfile b/Dockerfile index 67f4506..efbef67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.27 -ARG GO_VERSION=1.27.1 +ARG GO_VERSION=1.27.2 ARG NPM_VERSION=12.0.2 ARG PLAYWRIGHT_VERSION=1.62.1 diff --git a/README.md b/README.md index 2479458..d7ac1c5 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Prebuilt archives for macOS, Linux, and Windows are available on the [GitHub rel Requires Go 1.27 or newer. -The preferred build toolchain is Go 1.27.1; CI also verifies the Go 1.27.0 source minimum. +The preferred build toolchain is Go 1.27.2; CI also verifies the Go 1.27.0 source minimum. macOS release checks use the Apple Silicon `xcode-27` runner, preserve the macOS 13 minimum, and execute Intel binaries under Rosetta. ```sh go test ./... diff --git a/docs/RELEASING.md b/docs/RELEASING.md index d0b70c1..d8b99a1 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -4,7 +4,7 @@ summary: 'Release checklist for ordercli (GitHub release + Homebrew tap)' # Releasing ordercli -Dispatch `.github/workflows/release.yml` on the current protected `main` with a SemVer `version`. The caller pins the shared Go CLI archetype at v1.9.0 (`f613cbfed2b043159c850c353e7facb8c89833b0`). It freezes the source, creates the annotated `v` tag, builds with GoReleaser, signs and notarizes macOS binaries, verifies on native arm64 and Intel runners, then publishes and updates `steipete/homebrew-tap`. +Dispatch `.github/workflows/release.yml` on the current protected `main` with a SemVer `version`. The caller pins the shared Go CLI archetype at v1.11.8 (`fc49859568b3781af7663bee48c003fd987dc08b`). It builds, signs, and verifies on `xcode-27`, using Rosetta for Intel execution. Dispatches default to `dry_run: true`, which does not create tags, publish releases, or update Homebrew. Explicit publication freezes the source, creates the annotated `v` tag, then publishes verified artifacts and updates `steipete/homebrew-tap`. The signing policy is `personal`: Developer ID Application: Peter Steinberger, team `Y5PE65HELJ`, identifier `com.steipete.ordercli.ordercli`. Native archives retain `ordercli___` names, tar.gz/Windows ZIP formats, and the README, license, and changelog. The release includes `checksums.txt`, `ASSET-INVENTORY.json`, `SIGNING-MANIFEST.json`, and `RELEASE-NOTES.md`; independent verifier attestations bind publication to the frozen source and exact signed bytes. @@ -25,7 +25,7 @@ The release builds on macOS so the GoReleaser post-build hook can inspect each a ## Publish - Confirm the version does not already exist as a local/remote tag or GitHub release. -- Dispatch `gh workflow run release.yml --ref main -f version=`. The workflow owns annotated tag creation; do not create or push a tag manually. +- Dispatch `gh workflow run release.yml --ref main -f version= -f dry_run=false`. The workflow owns annotated tag creation; do not create or push a tag manually. For verification only, use the current version with `-f dry_run=true`. - Watch the release workflow, including its Homebrew handoff, to completion. - The shared workflow publishes the finalized changelog section as both the release body and `RELEASE-NOTES.md`. Check it against `./scripts/release-notes v`; preserve those exact notes after publication. diff --git a/go.mod b/go.mod index 738ab6e..d652d6f 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/steipete/ordercli go 1.27.0 -toolchain go1.27.1 +toolchain go1.27.2 require ( github.com/spf13/cobra v1.10.2 diff --git a/internal/cli/glovo_cmd_test.go b/internal/cli/glovo_cmd_test.go index eb48d58..004d698 100644 --- a/internal/cli/glovo_cmd_test.go +++ b/internal/cli/glovo_cmd_test.go @@ -16,7 +16,8 @@ func TestGlovoCLI_ConfigSetAndShow(t *testing.T) { cfgPath := filepath.Join(t.TempDir(), "config.json") // Test config set - out, _, err := runCLI(cfgPath, []string{"glovo", "config", "set", + out, _, err := runCLI(cfgPath, []string{ + "glovo", "config", "set", "--city-code", "MAD", "--country-code", "ES", "--language", "en", diff --git a/internal/deliveroo/public_status.go b/internal/deliveroo/public_status.go index 210d05a..7be3870 100644 --- a/internal/deliveroo/public_status.go +++ b/internal/deliveroo/public_status.go @@ -9,8 +9,10 @@ import ( "github.com/steipete/ordercli/internal/browserpage" ) -var quantityOnlyRe = regexp.MustCompile(`^\d+\s*x$`) -var readBrowserPageText = browserpage.ReadText +var ( + quantityOnlyRe = regexp.MustCompile(`^\d+\s*x$`) + readBrowserPageText = browserpage.ReadText +) type PublicStatus struct { URL string `json:"url,omitempty"` diff --git a/internal/deliveroo/public_status_test.go b/internal/deliveroo/public_status_test.go index c7e336a..03ff2e5 100644 --- a/internal/deliveroo/public_status_test.go +++ b/internal/deliveroo/public_status_test.go @@ -160,7 +160,7 @@ func TestFetchPublicStatus(t *testing.T) { return browserpage.Result{ FinalURL: "https://deliveroo.example/final", Title: "Deliveroo", - Text: "This order is for Gildas\nDelivery\n221 Pentonville Road, FORA, N19UZ\nOrder Details\nTa'Mini\nOrder #5040", + Text: "This order is for Gildas\nDelivery\n221 Pentonville Road, FORA, N19UZ\nOrder Details\nTa'Mini\nOrder #5040", }, nil } diff --git a/internal/glovo/models.go b/internal/glovo/models.go index d4220d4..277bddd 100644 --- a/internal/glovo/models.go +++ b/internal/glovo/models.go @@ -69,19 +69,19 @@ type BasketsResponse []Basket // Basket represents a shopping cart for a store type Basket struct { - StoreID int `json:"storeId"` - StoreAddressID int `json:"storeAddressId"` - StoreName string `json:"storeName"` - StoreSlug string `json:"storeSlug"` - Products []BasketItem `json:"products"` - SubTotal float64 `json:"subTotal"` - DeliveryFee float64 `json:"deliveryFee"` - ServiceFee float64 `json:"serviceFee"` - SmallOrderFee float64 `json:"smallOrderFee"` - Total float64 `json:"total"` - Currency string `json:"currency"` - MinOrderValue float64 `json:"minOrderValue"` - IsMinOrderMet bool `json:"isMinOrderMet"` + StoreID int `json:"storeId"` + StoreAddressID int `json:"storeAddressId"` + StoreName string `json:"storeName"` + StoreSlug string `json:"storeSlug"` + Products []BasketItem `json:"products"` + SubTotal float64 `json:"subTotal"` + DeliveryFee float64 `json:"deliveryFee"` + ServiceFee float64 `json:"serviceFee"` + SmallOrderFee float64 `json:"smallOrderFee"` + Total float64 `json:"total"` + Currency string `json:"currency"` + MinOrderValue float64 `json:"minOrderValue"` + IsMinOrderMet bool `json:"isMinOrderMet"` } // BasketItem represents an item in the cart @@ -97,40 +97,40 @@ type BasketItem struct { // UserResponse represents the response from /v3/me type UserResponse struct { - ID int `json:"id"` - Type string `json:"type"` - URN string `json:"urn"` - Name string `json:"name"` - Picture *string `json:"picture"` - Email string `json:"email"` - Description *string `json:"description"` - FacebookID *string `json:"facebookId"` - PreferredCityCode string `json:"preferredCityCode"` - PreferredLanguage string `json:"preferredLanguage"` - PreferredLanguageRegion string `json:"preferredLanguageRegion"` - Locale string `json:"locale"` - DeviceURN *string `json:"deviceUrn"` - AnalyticsID *string `json:"analyticsId"` - MediaCampaign *string `json:"mediaCampaign"` - MediaSource *string `json:"mediaSource"` - OS *string `json:"os"` - DeliveredOrdersCount int `json:"deliveredOrdersCount"` - PhoneNumber *PhoneNumber `json:"phoneNumber"` - CompanyDetail *string `json:"companyDetail"` - VirtualBalance *Balance `json:"virtualBalance"` - FreeOrders int `json:"freeOrders"` - PaymentMethod string `json:"paymentMethod"` - PaymentWay string `json:"paymentWay"` - CurrentCard *string `json:"currentCard"` - AccumulatedDebt float64 `json:"accumulatedDebt"` - Defaulter bool `json:"defaulter"` - Gender *string `json:"gender"` - AgeMin *int `json:"ageMin"` - AgeMax *int `json:"ageMax"` - Birthday *string `json:"birthday"` - PrivacySettings any `json:"privacySettings"` - Permissions []Permission `json:"permissions"` - DataPrivacyEnabled bool `json:"dataPrivacyEnabled"` + ID int `json:"id"` + Type string `json:"type"` + URN string `json:"urn"` + Name string `json:"name"` + Picture *string `json:"picture"` + Email string `json:"email"` + Description *string `json:"description"` + FacebookID *string `json:"facebookId"` + PreferredCityCode string `json:"preferredCityCode"` + PreferredLanguage string `json:"preferredLanguage"` + PreferredLanguageRegion string `json:"preferredLanguageRegion"` + Locale string `json:"locale"` + DeviceURN *string `json:"deviceUrn"` + AnalyticsID *string `json:"analyticsId"` + MediaCampaign *string `json:"mediaCampaign"` + MediaSource *string `json:"mediaSource"` + OS *string `json:"os"` + DeliveredOrdersCount int `json:"deliveredOrdersCount"` + PhoneNumber *PhoneNumber `json:"phoneNumber"` + CompanyDetail *string `json:"companyDetail"` + VirtualBalance *Balance `json:"virtualBalance"` + FreeOrders int `json:"freeOrders"` + PaymentMethod string `json:"paymentMethod"` + PaymentWay string `json:"paymentWay"` + CurrentCard *string `json:"currentCard"` + AccumulatedDebt float64 `json:"accumulatedDebt"` + Defaulter bool `json:"defaulter"` + Gender *string `json:"gender"` + AgeMin *int `json:"ageMin"` + AgeMax *int `json:"ageMax"` + Birthday *string `json:"birthday"` + PrivacySettings any `json:"privacySettings"` + Permissions []Permission `json:"permissions"` + DataPrivacyEnabled bool `json:"dataPrivacyEnabled"` } // PhoneNumber represents a phone number with country code