Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

## Testing

- [ ] `go test -race ./...`
- [ ] `go vet ./...`
- [ ] `git diff --check`
- [ ] `make check`
- [ ] `govulncheck ./...`
- [ ] Provider-contract changes include updated provenance/version/`last_verified` metadata

## Security and privacy
Expand Down
33 changes: 31 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,36 @@ jobs:
run: |
unformatted="$(find . -name '*.go' -type f -exec gofmt -l {} +)"
test -z "$unformatted"
- run: go test -race -shuffle=on ./...
- name: Test with coverage
if: runner.os == 'Linux'
run: go test -race -shuffle=on -coverprofile=coverage.out ./...
- name: Enforce coverage floor
if: runner.os == 'Linux'
shell: bash
run: |
coverage="$(go tool cover -func=coverage.out | awk '/^total:/ {gsub(/%/, "", $3); print $3}')"
echo "total statement coverage: ${coverage}%"
awk -v coverage="$coverage" 'BEGIN { exit !(coverage + 0 >= 70) }'
- name: Test
if: runner.os != 'Linux'
run: go test -race -shuffle=on ./...
- run: go vet ./...
- run: git diff --check
lint:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.12.2
security:
needs: changes
if: needs.changes.outputs.code == 'true'
Expand All @@ -99,7 +126,7 @@ jobs:
GOARCH: amd64
run: go build ./cmd/coned
required:
needs: [changes, test, security]
needs: [changes, test, lint, security]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
Expand All @@ -110,10 +137,12 @@ jobs:
CHANGE_RESULT: ${{ needs.changes.result }}
CODE_CHANGED: ${{ needs.changes.outputs.code }}
TEST_RESULT: ${{ needs.test.result }}
LINT_RESULT: ${{ needs.lint.result }}
SECURITY_RESULT: ${{ needs.security.result }}
run: |
test "$CHANGE_RESULT" = "success"
if [[ "$CODE_CHANGED" == "true" ]]; then
test "$TEST_RESULT" = "success"
test "$LINT_RESULT" = "success"
test "$SECURITY_RESULT" = "success"
fi
9 changes: 9 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: "2"

run:
timeout: 5m
tests: true

issues:
max-issues-per-linter: 0
max-same-issues: 0
13 changes: 8 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ For security vulnerabilities, follow [SECURITY.md](SECURITY.md) instead of openi

## Development

Go 1.25.12 or newer is required.
Go 1.25.12 or newer is required. Install the pinned lint runner once:

```bash
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
```

Then run:

```bash
go mod download
make fmt-check
make test
make vet
make diff-check
make check
make build
```

Expand Down
15 changes: 9 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
DIRTY ?= $(shell test -z "$$(git status --porcelain 2>/dev/null)" && echo false || echo true)
LDFLAGS := -X $(PKG).Version=$(VERSION) -X $(PKG).Commit=$(COMMIT) -X $(PKG).Date=$(DATE) -X $(PKG).Dirty=$(DIRTY)
GO ?= go

.PHONY: build install test vet security fmt-check diff-check release-check check clean
.PHONY: build install test vet lint security fmt-check diff-check release-check check clean
build:
mkdir -p $(BIN_DIR)
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(BINARY) $(CMD)
GOTOOLCHAIN=auto $(GO) build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(BINARY) $(CMD)
install:
go install -ldflags "$(LDFLAGS)" $(CMD)
GOTOOLCHAIN=auto $(GO) install -ldflags "$(LDFLAGS)" $(CMD)
test:
go test -race ./...
GOTOOLCHAIN=auto $(GO) test -race -shuffle=on ./...
vet:
go vet ./...
GOTOOLCHAIN=auto $(GO) vet ./...
lint:
GOTOOLCHAIN=auto golangci-lint run ./...
fmt-check:
test -z "$$(gofmt -l $$(find . -name '*.go' -type f))"
diff-check:
Expand All @@ -27,6 +30,6 @@ security:
release-check:
goreleaser check
goreleaser release --snapshot --clean
check: fmt-check test vet diff-check
check: fmt-check test vet lint diff-check
clean:
rm -rf $(BIN_DIR)
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# coned-cli

[![CI](https://github.com/zzwong/coned-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/zzwong/coned-cli/actions/workflows/ci.yml)
[![golangci-lint](https://img.shields.io/github/actions/workflow/status/zzwong/coned-cli/ci.yml?branch=main&label=golangci-lint&logo=go)](https://github.com/zzwong/coned-cli/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

A headless CLI for accessing Con Edison account, billing, and energy-usage data.
Expand Down Expand Up @@ -212,10 +213,7 @@ CLI flags override the configured profile and timeout. The production client acc
## Development

```bash
make fmt-check
make test
make vet
make diff-check
make check
make build
./bin/coned version
```
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,11 @@ func authStatus(cmd *cobra.Command, profile string, jsonOutput bool, deps Depend
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(data))
return err
}
fmt.Fprintln(cmd.OutOrStdout(), statusName(state))
return nil
_, err = fmt.Fprintln(cmd.OutOrStdout(), statusName(state))
return err
}

func authLogout(cmd *cobra.Command, profile string, timeout time.Duration, deps Dependencies, forget bool) error {
Expand All @@ -299,6 +299,6 @@ func authLogout(cmd *cobra.Command, profile string, timeout time.Duration, deps
if len(errorsFound) > 0 {
return errors.Join(errorsFound...)
}
fmt.Fprintln(cmd.OutOrStdout(), "not authenticated")
return nil
_, err := fmt.Fprintln(cmd.OutOrStdout(), "not authenticated")
return err
}
12 changes: 6 additions & 6 deletions internal/cli/bills.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ func downloadBill(cmd *cobra.Command, options *Options, deps Dependencies, id, o
keep := false
defer func() {
if !keep {
file.Close()
os.Remove(temp)
_ = file.Close()
_ = os.Remove(temp)
}
}()
ctx, cancel := context.WithTimeout(cmd.Context(), options.Timeout)
Expand All @@ -136,8 +136,8 @@ func downloadBill(cmd *cobra.Command, options *Options, deps Dependencies, id, o
return coned.ErrProtocolChanged
}
keep = true
fmt.Fprintln(cmd.OutOrStdout(), output)
return nil
_, err = fmt.Fprintln(cmd.OutOrStdout(), output)
return err
}

func billDateFromID(id string) (string, bool) {
Expand Down Expand Up @@ -172,8 +172,8 @@ func newOutputFile(output string, force bool) (*os.File, string, error) {
return nil, "", coned.ErrProtocolChanged
}
if err := file.Chmod(0o600); err != nil {
file.Close()
os.Remove(file.Name())
_ = file.Close()
_ = os.Remove(file.Name())
return nil, "", coned.ErrProtocolChanged
}
return file, file.Name(), nil
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/bills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,11 @@ func TestConcreteDownloadFailuresCleanUpAtCLILayer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/en/accounts-billing/billing-history":
io.WriteString(w, `<input name="AccountMAID" value="synthetic-maid">`)
_, _ = io.WriteString(w, `<input name="AccountMAID" value="synthetic-maid">`)
case "/sitecore/api/ssc/ConEdWeb-Foundation-MyAccount-Areas-BillingHistory-BillingHistoryAPI/User/0/GetResidentialBillHistory":
io.WriteString(w, `[{"BillDate":"2026-02-15","DocumentId":"opaque-synthetic"}]`)
_, _ = io.WriteString(w, `[{"BillDate":"2026-02-15","DocumentId":"opaque-synthetic"}]`)
case "/sitecore/api/ssc/ConEdWeb-Foundation-MyAccount-Areas-BillingHistory-BillingHistoryAPI/User/0/BillInsertImage":
io.WriteString(w, tc.documentResponse)
_, _ = io.WriteString(w, tc.documentResponse)
default:
http.NotFound(w, r)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ func newDiagnosticsCommand(options *Options, deps Dependencies) *cobra.Command {
err = closeErr
}
if err != nil {
os.Remove(temp)
_ = os.Remove(temp)
return coned.ErrProtocolChanged
}
if err = publishOutput(temp, output, false); err != nil {
os.Remove(temp)
_ = os.Remove(temp)
return coned.ErrProtocolChanged
}
return nil
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ func selectEntity(cmd *cobra.Command, options *Options, deps Dependencies, value
if cfg.Save(deps.ConfigPath) != nil {
return coned.ErrProtocolChanged
}
fmt.Fprintln(cmd.OutOrStdout(), "selected "+strings.SplitN(value, "-", 2)[0])
return nil
_, err = fmt.Fprintln(cmd.OutOrStdout(), "selected "+strings.SplitN(value, "-", 2)[0])
return err
}
func resolveEntitySelection(ctx context.Context, options *Options, deps Dependencies, session auth.Session) (coned.EntitySelection, error) {
account, meter := options.Account, options.Meter
Expand Down
14 changes: 7 additions & 7 deletions internal/cli/green_button.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func greenExport(cmd *cobra.Command, o *Options, d Dependencies, format, from, t
if e != nil {
return safeGreenError(e)
}
defer os.Remove(spool)
defer func() { _ = os.Remove(spool) }()
if download {
if output == "" {
output = "green-button-usage.zip"
Expand All @@ -118,25 +118,25 @@ func greenExport(cmd *cobra.Command, o *Options, d Dependencies, format, from, t
keep := false
defer func() {
if !keep {
f.Close()
os.Remove(tmp)
_ = f.Close()
_ = os.Remove(tmp)
}
}()
in, e := os.Open(spool)
if e != nil {
return coned.ErrProtocolChanged
}
_, e = io.Copy(f, in)
in.Close()
if e != nil || f.Sync() != nil || f.Close() != nil {
closeErr := in.Close()
if e != nil || closeErr != nil || f.Sync() != nil || f.Close() != nil {
return coned.ErrProtocolChanged
}
if publishOutput(tmp, output, force) != nil {
return coned.ErrProtocolChanged
}
keep = true
fmt.Fprintln(cmd.OutOrStdout(), output)
return nil
_, e = fmt.Fprintln(cmd.OutOrStdout(), output)
return e
}
if format == "json" {
e = coned.GreenButtonCSVJSON(spool, cmd.OutOrStdout())
Expand Down
Loading
Loading