From 4c0a4be30010a131d673f9496d5b41efcd460047 Mon Sep 17 00:00:00 2001 From: Eric Searcy Date: Tue, 14 Jul 2026 10:47:34 -0700 Subject: [PATCH 1/8] Scaffold Go CLI application with stubbed auth command - Initialize Go module (github.com/linuxfoundation/lfx-cli) - Adopt urfave/cli/v3 for subcommand routing - Add urfave/cli-docs/v3 for Markdown doc generation via hidden 'lfx docs' - Stub out auth login/token/status/logout and api subcommands - Add Makefile mirroring lfx-mcp build/check conventions - Configure GoReleaser for linux/darwin/windows amd64+arm64 builds (windows/arm64 excluded) - Add release-tag workflow to publish binaries via GoReleaser on tag push, with per-job permissions and all actions pinned to commit SHA - Add MegaLinter workflow/config, .cspell.json, .lycheeignore, .yamllint, and .gitignore - Pin the license-header-check reusable workflow to a commit SHA (no tags published upstream) and disable Go module caching in the release workflow to address zizmor's unpinned-uses and cache-poisoning audits - Add AGENTS.md with build, test, and contribution guidance - Update README with install instructions and usage Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy --- .cspell.json | 56 ++++++ .github/workflows/license-header-check.yml | 5 +- .github/workflows/mega-linter.yml | 47 +++++ .github/workflows/release-tag.yml | 43 +++++ .gitignore | 122 ++++++++++++ .goreleaser.yaml | 52 ++++++ .lycheeignore | 6 + .mega-linter.yml | 16 ++ .yamllint | 11 ++ AGENTS.md | 204 +++++++++++++++++++++ Makefile | 127 +++++++++++++ README.md | 51 +++++- cmd/lfx/main.go | 36 ++++ go.mod | 15 ++ go.sum | 16 ++ internal/commands/api.go | 28 +++ internal/commands/auth.go | 74 ++++++++ internal/commands/docs.go | 55 ++++++ 18 files changed, 962 insertions(+), 2 deletions(-) create mode 100644 .cspell.json create mode 100644 .github/workflows/mega-linter.yml create mode 100644 .github/workflows/release-tag.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 .lycheeignore create mode 100644 .mega-linter.yml create mode 100644 .yamllint create mode 100644 AGENTS.md create mode 100644 Makefile create mode 100644 cmd/lfx/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/commands/api.go create mode 100644 internal/commands/auth.go create mode 100644 internal/commands/docs.go diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000..df79be4 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,56 @@ +{ + "language": "en", + "dictionaries": ["companies", "filetypes", "fullstack", "softwareTerms"], + "words": [ + "aquasecurity", + "artipacked", + "cimd", + "clidocs", + "coverprofile", + "cpuprof", + "ehthumbs", + "goarch", + "golangci", + "goreleaser", + "ldflags", + "lfx", + "lfxv", + "linuxfoundation", + "memprof", + "techdocs", + "urfave", + "zizmor" + ], + "flagWords": [ + "abort", + "abortion", + "blackhat", + "black-hat", + "whitehat", + "white-hat", + "cripple", + "crippled", + "master", + "slave", + "tribe", + "sanity-check", + "whitelist", + "white-list", + "blacklist", + "black-list" + ], + "ignorePaths": [ + ".cspell.json", + "CODEOWNERS", + "LICENSE", + "LICENSE-docs", + "megalinter-reports", + "styles" + ], + "languageSettings": [ + { + "languageId": "markdown", + "ignoreRegExpList": ["/^\\s*```\\s*(mermaid)[\\s\\S]*?^\\s*```/gm"] + } + ] +} diff --git a/.github/workflows/license-header-check.yml b/.github/workflows/license-header-check.yml index ba4ba68..1026e8f 100644 --- a/.github/workflows/license-header-check.yml +++ b/.github/workflows/license-header-check.yml @@ -13,6 +13,9 @@ permissions: jobs: license-header-check: name: License Header Check - uses: linuxfoundation/lfx-public-workflows/.github/workflows/license-header-check.yml@main + # Pinned to main as of 2026-07-14 (no tagged releases are published for + # this repo); bump the SHA periodically to pick up upstream fixes. + uses: >- + linuxfoundation/lfx-public-workflows/.github/workflows/license-header-check.yml@8313b4b18b95d750f99a78d5ea160d851afbc40b with: copyright_line: "Copyright The Linux Foundation and each contributor to LFX." diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml new file mode 100644 index 0000000..66a695b --- /dev/null +++ b/.github/workflows/mega-linter.yml @@ -0,0 +1,47 @@ +--- +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +name: MegaLinter + +"on": + pull_request: null + +permissions: + contents: read + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + megalinter: + name: MegaLinter + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + + # MegaLinter + - name: MegaLinter + id: ml + # Use the Go flavor. Keep this version in sync with the + # `megalinter` target in the Makefile. + uses: oxsecurity/megalinter/flavors/go@ef3e84b8b836d76db562d0f3ed7da61e8fd538bc # v9.6.0 + env: + # All available variables are described in documentation + # https://megalinter.io/latest/configuration/ + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Allow GITHUB_TOKEN for working around rate limits (aquasecurity/trivy#7668). + REPOSITORY_TRIVY_UNSECURED_ENV_VARIABLES: GITHUB_TOKEN + # zizmor's "artipacked" audit needs the GitHub API (to verify + # SHA-pinned action tags), which requires GITHUB_TOKEN. + ACTION_ZIZMOR_UNSECURED_ENV_VARIABLES: GITHUB_TOKEN diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml new file mode 100644 index 0000000..a042e74 --- /dev/null +++ b/.github/workflows/release-tag.yml @@ -0,0 +1,43 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +name: Publish Tagged Release + +"on": + push: + tags: + - "v*" + +permissions: {} + +jobs: + goreleaser: + name: GoReleaser + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version-file: go.mod + # Disable caching: this workflow publishes runtime artifacts on + # tag push, so a poisoned cache from an earlier run could leak + # into a release build (zizmor cache-poisoning audit). + cache: false + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9374fa0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,122 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +# Binaries and executables +/bin/ +/dist/ +*.exe +*.exe~ +*.dll +*.so +*.dylib +/lfx + +# Go build artifacts +*.test +*.prof +*.pprof + +# Go coverage files +coverage.out +coverage.html +coverage.txt +*.coverage +c.out + +# Go module cache (when using vendor) +/vendor/ + +# Go workspace file +go.work +go.work.sum + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# Claude AI assistant +.claude + +# Environment and configuration files +.env +.env.local +.env.*.local +*.env +config.local.yaml +config.local.yml +secrets.yaml +secrets.yml + +# Log files +*.log +logs/ +*.log.* + +# Temporary files +*.tmp +*.temp +/tmp/ +.cache/ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Security and sensitive files +*.pem +*.key +*.crt +*.p12 +*.pfx +ca-certificates.crt +tls.crt +tls.key + +# Backup files +*.bak +*.backup +*.old +*~ + +# Profiling and debugging +*.pprof +pprof/ +*.memprof +*.cpuprof + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Archive files +*.tar +*.tar.gz +*.zip +*.rar +*.7z + +# CI/CD temporary files +.github/workflows/*.tmp +.gitlab-ci.tmp +.travis.tmp + +# Local development overrides +Makefile.local + +# Generated documentation +docs/_build/ +site/ + +# Lint files +megalinter-reports diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..81533d8 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,52 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - id: lfx + main: ./cmd/lfx + binary: lfx + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + ignore: + - goos: windows + goarch: arm64 + ldflags: + - -s -w -X main.version={{.Version}} + +archives: + - id: lfx + formats: [tar.gz] + format_overrides: + - goos: windows + formats: [zip] + name_template: >- + {{ .ProjectName }}_{{ .Os }}_{{ .Arch }} + +checksum: + name_template: "checksums.txt" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^ci:" + +release: + github: + owner: linuxfoundation + name: lfx-cli diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 0000000..fa175a2 --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,6 @@ +# Ignore local Kubernetes paths. +^https?://[a-zA-Z0-9.-]+\.svc\.cluster\.local + +# megalinter.io redirects (e.g. /configuration/ -> /latest/configuration/) and +# has been flaky/404-prone for lychee; not worth failing CI over. +^https?://megalinter\.io/ diff --git a/.mega-linter.yml b/.mega-linter.yml new file mode 100644 index 0000000..16b865c --- /dev/null +++ b/.mega-linter.yml @@ -0,0 +1,16 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +GITHUB_COMMENT_REPORTER: false +ENABLE_LINTERS: + - ACTION_ZIZMOR +DISABLE_LINTERS: + # Prefer Revive. + - GO_GOLANGCI_LINT + # yamllint is sufficient for us. + - YAML_PRETTIER +YAML_YAMLLINT_CONFIG_FILE: .yamllint +SPELL_CSPELL_ANALYZE_FILE_NAMES: false +# Ignore YAML files with templating macros; these typically fail linting and/or +# schema checking. +FILTER_REGEX_EXCLUDE: '(templates/.*\.yml|templates/.*\.yaml)' diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..acd4017 --- /dev/null +++ b/.yamllint @@ -0,0 +1,11 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +extends: default +ignore: | + .git + megalinter-reports +rules: + line-length: + max: 120 + level: warning diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..56809de --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,204 @@ +# AGENTS.md + +This file provides essential information for AI agents working on the LFX +CLI codebase. It focuses on development workflows, architecture +understanding, and build processes needed for making code changes. + +## Repository Overview + +The LFX CLI (`lfx`) is a developer-facing command-line tool for +authenticating with the Linux Foundation's LFX platform and making +authenticated API calls, following the same interaction model as the `gh` +CLI (`lfx auth login` → `lfx auth token`). + +### Key Technologies + +- **Language**: Go (see `go.mod` for the minimum required version) +- **CLI framework**: [`urfave/cli/v3`](https://github.com/urfave/cli) for + subcommand routing +- **Docs generation**: [`urfave/cli-docs/v3`](https://github.com/urfave/cli-docs) + for LLM/agent-friendly Markdown reference docs (`lfx docs`) +- **Release automation**: [GoReleaser](https://goreleaser.com/) for + multi-arch binary builds published to GitHub Releases + +## Architecture Overview + +```text +lfx-cli/ +├── cmd/ +│ └── lfx/ # Main application entry point +├── internal/ +│ └── commands/ # CLI subcommand implementations +├── .goreleaser.yaml # Multi-arch release build configuration +├── go.mod # Go module definition +├── Makefile # Build automation +├── README.md # User documentation +└── AGENTS.md # This file (AI agent guidelines) +``` + +### Current State + +This repo is under active scaffolding. Auth and API commands are currently +stubs; real implementations land in follow-on work: + +- `lfx auth login` / `status` / `logout` +- `lfx auth token` +- `lfx api` + +Credential storage (system keychain via `99designs/keyring`) and the Auth0 +CIMD client are tracked separately. + +**No container build**: this project produces binary artifacts only, +distributed via GitHub Releases and `go install`. There is no Dockerfile, +Helm chart, or container image pipeline. + +## Development Workflow + +### Prerequisites + +```bash +# Verify Go is installed at or above the version in go.mod. +go version +``` + +### Common Development Tasks + +#### 1. Build the CLI + +```bash +make build +# or directly: go build -ldflags="-s -w" -o bin/lfx ./cmd/lfx +``` + +#### 2. Run the CLI + +```bash +make run ARGS="auth login" +# or directly: ./bin/lfx auth login +``` + +#### 3. Code Quality Checks + +```bash +make fmt # Format code +make vet # Run go vet +make lint # Run golangci-lint (if installed) +make revive # Run revive (if installed) +make goreleaser-check # Validate .goreleaser.yaml (if goreleaser installed) +make check # Run all of the above +``` + +#### 4. Tests + +```bash +make test # Run Go tests +make test-coverage # Run tests with coverage report +``` + +#### 5. Clean Build Artifacts + +```bash +make clean +``` + +## Adding New Commands + +Commands are implemented in `internal/commands` and registered with the +root `*cli.Command` in `cmd/lfx/main.go`. + +### Command Implementation Steps + +1. **Create or extend a file** in `internal/commands/` (e.g., `auth.go` for + an `auth` subcommand group) +2. **Implement a `NewCommand()` function** that returns a + `*cli.Command`, with nested `Commands` for subcommand groups +3. **Register it** in `cmd/lfx/main.go`'s `Commands` slice + +### Example Command Implementation + +```go +// Package commands implements the lfx CLI subcommands. +package commands + +import ( + "context" + "fmt" + + "github.com/urfave/cli/v3" +) + +// NewExampleCommand builds the `lfx example` command. +func NewExampleCommand() *cli.Command { + return &cli.Command{ + Name: "example", + Usage: "Brief description of what the command does", + Action: func(_ context.Context, cmd *cli.Command) error { + fmt.Println("example: not yet implemented") + return nil + }, + } +} +``` + +### Package Comments + +Every file in a package must start with the same `// Package ...` +doc comment immediately above the `package` declaration (required by +MegaLinter's revive `package-comments` rule). Do not vary the wording +between files in the same package. + +## Documentation Generation + +The hidden `lfx docs` command generates Markdown reference documentation +for all commands via `cli-docs.ToMarkdown()`, intended for local agent use +and future publishing to the `gh-pages` branch: + +```bash +lfx docs # Print to stdout +lfx docs --output ./docs # Write to ./docs/cli.md +``` + +## Release Process + +Releases follow [semantic versioning](https://semver.org/) (`vMAJOR.MINOR.PATCH`). +The current series is v0.x; do not increment the major version unless +explicitly instructed. + +### Version bump guidelines + +| Change type | Version component | +|--------------------------------------------------------------------------------------|----------------------------| +| Bug fixes, help text/schema wording tweaks, operational changes (CI, release config) | **patch** | +| New commands or substantial updates to existing commands | **minor** | +| Breaking changes or explicit instruction | **major** (only when told) | + +### Cutting a release + +Do **not** create or push git tags manually. Instead, use the GitHub +Releases UI (or `gh` CLI) to create a release; GitHub will create the tag +automatically, and the **Publish Tagged Release** GitHub Actions workflow +will run GoReleaser to build and publish multi-arch binaries. + +```bash +# Determine the next version by inspecting the latest tag. +LATEST=$(git tag --sort=-v:refname | head -1) +echo "Latest tag: $LATEST" +NEXT=v0.1.1 # bump appropriately from the latest tag + +gh release create "$NEXT" \ + --generate-notes \ + --latest +``` + +After creating the release, verify that the **Publish Tagged Release** +workflow triggered by the new tag completes successfully; otherwise +release binaries may be missing even though the GitHub Release exists. + +## Contributing Guidelines + +1. **Add Commands**: Create new commands in `internal/commands/` following + the established pattern +2. **Package Comments**: Every new `*.go` file must include the same + `// Package ...` doc comment as the rest of its package +3. **Code Quality**: Run `make check` before commits +4. **Documentation**: Update README.md for user-facing changes diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1c0de94 --- /dev/null +++ b/Makefile @@ -0,0 +1,127 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +.PHONY: all build clean check fmt vet lint revive goreleaser-check test test-coverage run deps install-tools megalinter help + +# Build variables +BINARY_NAME=lfx +CMD_DIR=./cmd/lfx +BUILD_DIR=./bin +GO_FILES=$(shell find . -name "*.go" -type f) + +# Version string: clean tag on a tagged commit, tag+offset+hash between tags, +# with a -dirty suffix if there are uncommitted changes. +VERSION := $(shell git describe --tags --dirty --always 2>/dev/null || echo "dev") + +# Build flags +LDFLAGS=-ldflags="-s -w -X main.version=$(VERSION)" + +# Default target +all: clean check build + +# Build the binary +build: $(BUILD_DIR)/$(BINARY_NAME) + +$(BUILD_DIR)/$(BINARY_NAME): $(GO_FILES) + @echo "Building $(BINARY_NAME)..." + @mkdir -p $(BUILD_DIR) + go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_DIR) + +# Clean build artifacts +clean: + @echo "Cleaning build artifacts..." + @rm -rf $(BUILD_DIR) + +# Run all checks +check: fmt vet lint revive goreleaser-check + +# Format Go code +fmt: + @echo "Formatting Go code..." + go fmt ./... + +# Run go vet +vet: + @echo "Running go vet..." + go vet ./... + +# Run golangci-lint (if available) +lint: + @echo "Running linters..." + @if command -v golangci-lint >/dev/null 2>&1; then \ + golangci-lint run; \ + else \ + echo "golangci-lint not installed, skipping..."; \ + fi + +# Run revive (if available) +revive: + @echo "Running revive..." + @if command -v revive >/dev/null 2>&1; then \ + revive ./...; \ + else \ + echo "revive not installed, skipping..."; \ + fi + +# Validate the GoReleaser config (if available) +goreleaser-check: + @echo "Validating .goreleaser.yaml..." + @if command -v goreleaser >/dev/null 2>&1; then \ + goreleaser check; \ + else \ + echo "goreleaser not installed, skipping..."; \ + fi + +# Run tests +test: + @echo "Running tests..." + go test -v ./... + +# Run tests with coverage +test-coverage: + @echo "Running tests with coverage..." + go test -v -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + +# Build and run the CLI +run: build + @echo "Running lfx..." + $(BUILD_DIR)/$(BINARY_NAME) $(ARGS) + +# Download dependencies +deps: + @echo "Downloading dependencies..." + go mod download + go mod tidy + +# Install development tools +install-tools: + @echo "Installing development tools..." + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go install github.com/goreleaser/goreleaser/v2@latest + +# Run MegaLinter locally via Docker (matches CI Go flavor at v9.6.0). +# Keep this version in sync with .github/workflows/mega-linter.yml. +megalinter: + docker pull ghcr.io/oxsecurity/megalinter-go:v9.6.0 + docker run --rm --platform linux/amd64 -v '$(CURDIR):/tmp/lint:rw' ghcr.io/oxsecurity/megalinter-go:v9.6.0 + +# Show help +help: + @echo "Available targets:" + @echo " all - Clean, check, and build (default)" + @echo " build - Build the binary" + @echo " clean - Clean build artifacts" + @echo " check - Run all code quality checks" + @echo " fmt - Format Go code" + @echo " vet - Run go vet" + @echo " lint - Run golangci-lint" + @echo " revive - Run revive" + @echo " goreleaser-check - Validate .goreleaser.yaml" + @echo " test - Run tests" + @echo " test-coverage - Run tests with coverage report" + @echo " run - Build and run the CLI (pass args via ARGS=...)" + @echo " deps - Download and tidy dependencies" + @echo " install-tools - Install development tools" + @echo " megalinter - Run MegaLinter locally via Docker" + @echo " help - Show this help message" diff --git a/README.md b/README.md index 2464966..feff76f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,52 @@ # LFX CLI -LFX CLI for authenticating with the LFX platform and making API calls +`lfx` is a developer-facing command-line tool for authenticating with the +Linux Foundation's LFX platform and making authenticated API calls, following +the same interaction model as the `gh` CLI (`lfx auth login` → `lfx auth +token`). + +## Installation + +```bash +go install github.com/linuxfoundation/lfx-cli/cmd/lfx@latest +``` + +Prebuilt binaries for Linux, macOS, and Windows are also published on the +[Releases](https://github.com/linuxfoundation/lfx-cli/releases) page. + +## Usage + +```bash +# Log in via the Auth0 Device Code flow. +lfx auth login + +# Show the current authentication status. +lfx auth status + +# Print a valid access token (e.g. for use in scripts or other tools). +lfx auth token + +# Log out and remove stored credentials. +lfx auth logout + +# Make an authenticated call to an LFX platform API endpoint. +lfx api +``` + +Run `lfx --help` or `lfx --help` for full details on any command. + +> **Note:** This project is under active development. Authentication and API +> commands are currently stubs; see the +> [LFXV2-2509 epic](https://linuxfoundation.atlassian.net/browse/LFXV2-2509) +> for status. + +## Development + +```bash +make build # Build ./bin/lfx +make check # Format, vet, and lint +make test # Run tests +``` + +See [AGENTS.md](AGENTS.md) for detailed development workflows and +architecture notes. diff --git a/cmd/lfx/main.go b/cmd/lfx/main.go new file mode 100644 index 0000000..77318a8 --- /dev/null +++ b/cmd/lfx/main.go @@ -0,0 +1,36 @@ +// Copyright The Linux Foundation and contributors. +// SPDX-License-Identifier: MIT + +// Package main provides the lfx CLI binary entry point. +package main + +import ( + "context" + "fmt" + "os" + + "github.com/linuxfoundation/lfx-cli/internal/commands" + "github.com/urfave/cli/v3" +) + +// version is set at build time via -ldflags. +var version = "dev" + +func main() { + app := &cli.Command{ + Name: "lfx", + Usage: "Authenticate with and call the LFX platform APIs", + Version: version, + EnableShellCompletion: true, + Commands: []*cli.Command{ + commands.NewAuthCommand(), + commands.NewAPICommand(), + commands.NewDocsCommand(), + }, + } + + if err := app.Run(context.Background(), os.Args); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..991cf0b --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT +module github.com/linuxfoundation/lfx-cli + +go 1.26.5 + +require ( + github.com/urfave/cli-docs/v3 v3.1.0 + github.com/urfave/cli/v3 v3.10.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b0ce4fa --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= +github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= +github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= +github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/commands/api.go b/internal/commands/api.go new file mode 100644 index 0000000..334d1e6 --- /dev/null +++ b/internal/commands/api.go @@ -0,0 +1,28 @@ +// Copyright The Linux Foundation and contributors. +// SPDX-License-Identifier: MIT + +// Package commands implements the lfx CLI subcommands. +package commands + +import ( + "context" + "fmt" + + "github.com/urfave/cli/v3" +) + +// NewAPICommand builds the `lfx api` command for making raw authenticated +// calls against LFX platform APIs. +// +// This is currently a stub; the real implementation lands in LFXV2-2517. +func NewAPICommand() *cli.Command { + return &cli.Command{ + Name: "api", + Usage: "Make an authenticated call to an LFX platform API endpoint", + ArgsUsage: " ", + Action: func(_ context.Context, _ *cli.Command) error { + fmt.Println("lfx api: not yet implemented (see LFXV2-2517)") + return nil + }, + } +} diff --git a/internal/commands/auth.go b/internal/commands/auth.go new file mode 100644 index 0000000..d358b4b --- /dev/null +++ b/internal/commands/auth.go @@ -0,0 +1,74 @@ +// Copyright The Linux Foundation and contributors. +// SPDX-License-Identifier: MIT + +// Package commands implements the lfx CLI subcommands. +package commands + +import ( + "context" + "fmt" + + "github.com/urfave/cli/v3" +) + +// NewAuthCommand builds the `lfx auth` command group with its subcommands. +// +// All subcommands are currently stubs; real implementations land in +// LFXV2-2513 (Auth0 client), LFXV2-2514 (keychain storage), LFXV2-2515 +// (login flow), and LFXV2-2516 (token command). +func NewAuthCommand() *cli.Command { + return &cli.Command{ + Name: "auth", + Usage: "Manage authentication with the LFX platform", + Commands: []*cli.Command{ + newAuthLoginCommand(), + newAuthTokenCommand(), + newAuthStatusCommand(), + newAuthLogoutCommand(), + }, + } +} + +func newAuthLoginCommand() *cli.Command { + return &cli.Command{ + Name: "login", + Usage: "Log in to the LFX platform via the Auth0 Device Code flow", + Action: func(_ context.Context, _ *cli.Command) error { + fmt.Println("lfx auth login: not yet implemented (see LFXV2-2515)") + return nil + }, + } +} + +func newAuthTokenCommand() *cli.Command { + return &cli.Command{ + Name: "token", + Usage: "Print a valid access token for the LFX platform", + Action: func(_ context.Context, _ *cli.Command) error { + fmt.Println("lfx auth token: not yet implemented (see LFXV2-2516)") + return nil + }, + } +} + +func newAuthStatusCommand() *cli.Command { + return &cli.Command{ + Name: "status", + Usage: "Show the current authentication status", + Action: func(_ context.Context, _ *cli.Command) error { + fmt.Println("lfx auth status: not yet implemented (see LFXV2-2515)") + return nil + }, + } +} + +func newAuthLogoutCommand() *cli.Command { + return &cli.Command{ + Name: "logout", + Usage: "Remove stored LFX platform credentials", + Action: func(_ context.Context, _ *cli.Command) error { + fmt.Println("lfx auth logout: not yet implemented (see LFXV2-2515)") + return nil + }, + } +} diff --git a/internal/commands/docs.go b/internal/commands/docs.go new file mode 100644 index 0000000..fd5b968 --- /dev/null +++ b/internal/commands/docs.go @@ -0,0 +1,55 @@ +// Copyright The Linux Foundation and contributors. +// SPDX-License-Identifier: MIT + +// Package commands implements the lfx CLI subcommands. +package commands + +import ( + "context" + "fmt" + "os" + "path/filepath" + + clidocs "github.com/urfave/cli-docs/v3" + "github.com/urfave/cli/v3" +) + +// NewDocsCommand builds the hidden `lfx docs` command used to generate +// LLM/agent-friendly Markdown reference documentation for all commands. +func NewDocsCommand() *cli.Command { + return &cli.Command{ + Name: "docs", + Usage: "Generate Markdown reference documentation for all commands", + Hidden: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "output", + Usage: "Directory to write docs/cli.md to; omit to print to stdout", + }, + }, + Action: func(_ context.Context, cmd *cli.Command) error { + md, err := clidocs.ToMarkdown(cmd.Root()) + if err != nil { + return fmt.Errorf("generating markdown docs: %w", err) + } + + out := cmd.String("output") + if out == "" { + fmt.Println(md) + return nil + } + + if err := os.MkdirAll(out, 0o755); err != nil { + return fmt.Errorf("creating output directory %q: %w", out, err) + } + + path := filepath.Join(out, "cli.md") + if err := os.WriteFile(path, []byte(md), 0o644); err != nil { + return fmt.Errorf("writing %q: %w", path, err) + } + + fmt.Println("Wrote", path) + return nil + }, + } +} From 21ebf82eb54c45b647a14ee63f39cc4ae14f0619 Mon Sep 17 00:00:00 2001 From: Eric Searcy Date: Tue, 14 Jul 2026 10:58:40 -0700 Subject: [PATCH 2/8] Fix license header wording to match license-header-check config Go source files used 'Copyright The Linux Foundation and contributors.' but the reusable license-header-check workflow is configured with the longer 'Copyright The Linux Foundation and each contributor to LFX.' string, causing CI to flag all four new Go files as missing their license header. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy --- cmd/lfx/main.go | 2 +- internal/commands/api.go | 2 +- internal/commands/auth.go | 2 +- internal/commands/docs.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/lfx/main.go b/cmd/lfx/main.go index 77318a8..941b026 100644 --- a/cmd/lfx/main.go +++ b/cmd/lfx/main.go @@ -1,4 +1,4 @@ -// Copyright The Linux Foundation and contributors. +// Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT // Package main provides the lfx CLI binary entry point. diff --git a/internal/commands/api.go b/internal/commands/api.go index 334d1e6..cea6f7f 100644 --- a/internal/commands/api.go +++ b/internal/commands/api.go @@ -1,4 +1,4 @@ -// Copyright The Linux Foundation and contributors. +// Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT // Package commands implements the lfx CLI subcommands. diff --git a/internal/commands/auth.go b/internal/commands/auth.go index d358b4b..2092839 100644 --- a/internal/commands/auth.go +++ b/internal/commands/auth.go @@ -1,4 +1,4 @@ -// Copyright The Linux Foundation and contributors. +// Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT // Package commands implements the lfx CLI subcommands. diff --git a/internal/commands/docs.go b/internal/commands/docs.go index fd5b968..2f6594e 100644 --- a/internal/commands/docs.go +++ b/internal/commands/docs.go @@ -1,4 +1,4 @@ -// Copyright The Linux Foundation and contributors. +// Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT // Package commands implements the lfx CLI subcommands. From a705a1d00cd9b450f54532b535b840610c68c391 Mon Sep 17 00:00:00 2001 From: Eric Searcy Date: Tue, 14 Jul 2026 11:09:12 -0700 Subject: [PATCH 3/8] Address Copilot review feedback - Remove stray ENABLE_LINTERS allow-list from .mega-linter.yml that was silently restricting MegaLinter to only run zizmor - Flatten the Makefile build target so it always rebuilds instead of relying on a stale file-timestamp prerequisite that misses git revision, deleted files, and go.mod changes - Point install-tools at the golangci-lint/v2 module path instead of the obsolete v1 module - Constrain the GoReleaser action version to '~> v2' instead of 'latest', so a future v3 release can't silently break old tags Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy --- .github/workflows/release-tag.yml | 4 +++- .mega-linter.yml | 2 -- Makefile | 7 ++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index a042e74..05dc550 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -37,7 +37,9 @@ jobs: - name: Run GoReleaser uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: - version: latest + # Constrained to v2 (the action's documented default) so a + # future GoReleaser v3 release doesn't silently break old tags. + version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.mega-linter.yml b/.mega-linter.yml index 16b865c..823c475 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -2,8 +2,6 @@ # SPDX-License-Identifier: MIT --- GITHUB_COMMENT_REPORTER: false -ENABLE_LINTERS: - - ACTION_ZIZMOR DISABLE_LINTERS: # Prefer Revive. - GO_GOLANGCI_LINT diff --git a/Makefile b/Makefile index 1c0de94..2551192 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,6 @@ BINARY_NAME=lfx CMD_DIR=./cmd/lfx BUILD_DIR=./bin -GO_FILES=$(shell find . -name "*.go" -type f) # Version string: clean tag on a tagged commit, tag+offset+hash between tags, # with a -dirty suffix if there are uncommitted changes. @@ -20,9 +19,7 @@ LDFLAGS=-ldflags="-s -w -X main.version=$(VERSION)" all: clean check build # Build the binary -build: $(BUILD_DIR)/$(BINARY_NAME) - -$(BUILD_DIR)/$(BINARY_NAME): $(GO_FILES) +build: @echo "Building $(BINARY_NAME)..." @mkdir -p $(BUILD_DIR) go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_DIR) @@ -97,7 +94,7 @@ deps: # Install development tools install-tools: @echo "Installing development tools..." - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest go install github.com/goreleaser/goreleaser/v2@latest # Run MegaLinter locally via Docker (matches CI Go flavor at v9.6.0). From 62890ace38f8ef33b85c842e8d491038aff7b8e8 Mon Sep 17 00:00:00 2001 From: Eric Searcy Date: Tue, 14 Jul 2026 13:05:19 -0700 Subject: [PATCH 4/8] Address second round of Copilot review feedback - Fall back to the module version from debug.ReadBuildInfo() when no -ldflags version is injected (e.g. 'make build' or 'go install ...@latest'), instead of always reporting 'dev' - Drop the Makefile's separate 'git describe' VERSION/-X main.version injection now that main.go derives the same information from Go's automatic VCS build-info stamping; GoReleaser still injects the precise tagged version via -ldflags - Serialize 'all' (clean/check/build) and 'check' (fmt before the read-only checks) via recursive $(MAKE) calls so 'make -j' can't race on bin/lfx or on source files being formatted while read Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy --- Makefile | 30 +++++++++++++++++++----------- cmd/lfx/main.go | 20 ++++++++++++++++++-- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 2551192..6602dfe 100644 --- a/Makefile +++ b/Makefile @@ -8,15 +8,20 @@ BINARY_NAME=lfx CMD_DIR=./cmd/lfx BUILD_DIR=./bin -# Version string: clean tag on a tagged commit, tag+offset+hash between tags, -# with a -dirty suffix if there are uncommitted changes. -VERSION := $(shell git describe --tags --dirty --always 2>/dev/null || echo "dev") - -# Build flags -LDFLAGS=-ldflags="-s -w -X main.version=$(VERSION)" - -# Default target -all: clean check build +# Build flags. Version is not injected via -ldflags: `go build` already +# embeds VCS info (module pseudo-version, revision, dirty state) into the +# binary's build info, which main.go reads via debug.ReadBuildInfo() as a +# fallback. This keeps `make build` and `go install ...@latest` consistent. +LDFLAGS=-ldflags="-s -w" + +# Default target. Recursive $(MAKE) calls serialize the three phases so +# `make -j all` reliably leaves a checked, up-to-date binary even under +# parallel make (clean/build/check would otherwise race on bin/lfx and +# on source files being formatted while read). +all: + $(MAKE) clean + $(MAKE) check + $(MAKE) build # Build the binary build: @@ -29,8 +34,11 @@ clean: @echo "Cleaning build artifacts..." @rm -rf $(BUILD_DIR) -# Run all checks -check: fmt vet lint revive goreleaser-check +# Run all checks. fmt rewrites source files, so it must complete before +# the read-only checks run; those are safe to parallelize with each other. +check: + $(MAKE) fmt + $(MAKE) vet lint revive goreleaser-check # Format Go code fmt: diff --git a/cmd/lfx/main.go b/cmd/lfx/main.go index 941b026..6a34b18 100644 --- a/cmd/lfx/main.go +++ b/cmd/lfx/main.go @@ -8,13 +8,29 @@ import ( "context" "fmt" "os" + "runtime/debug" "github.com/linuxfoundation/lfx-cli/internal/commands" "github.com/urfave/cli/v3" ) -// version is set at build time via -ldflags. -var version = "dev" +// version is set at build time via -ldflags by GoReleaser. When unset (e.g. +// `make build` or `go install github.com/linuxfoundation/lfx-cli/cmd/lfx@latest`, +// neither of which supply linker flags), it falls back to the module +// version recorded in the binary's build info, or "dev" if that's +// unavailable. +var version = "" + +func init() { + if version != "" { + return + } + if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" { + version = info.Main.Version + return + } + version = "dev" +} func main() { app := &cli.Command{ From 5eb7ca2c5ac607e359fd1b98e88df3af89f5e6f9 Mon Sep 17 00:00:00 2001 From: Eric Searcy Date: Tue, 14 Jul 2026 13:37:11 -0700 Subject: [PATCH 5/8] fix(review): address PR #1 review feedback round 2 Address review comments from copilot-pull-request-reviewer: - .github/workflows/build.yaml: added a standalone Build workflow that runs go build, gofmt -l, go vet, and go test on every PR, since MegaLinter's revive-only run (GO_GOLANGCI_LINT disabled) never compiles the Go packages, so CI could otherwise pass non-building code (per copilot-pull-request-reviewer) - AGENTS.md: clarified the Package Comments section to explain that MegaLinter's GO_REVIVE_CLI_LINT_MODE defaults to list_of_files (verified locally with revive 1.15.0), under which revive loses per-package grouping and requires the doc comment on every file, rather than overstating Revive's own package-comments rule (per copilot-pull-request-reviewer) - Makefile: reworded the build-flags comment to accurately describe that a local build's pseudo-version already encodes VCS revision and dirty state (verified via go version -m), while go install of a tagged version reports the resolved tag in a different format (per copilot-pull-request-reviewer) No change made to the release-tag.yml/AGENTS.md release procedure (flagged as a possible workflow trigger issue): `gh release create` run by a human triggers the tag-push workflow normally; the GITHUB_TOKEN recursion guard only suppresses workflow-triggered-workflow chains, not human-driven API calls. This matches the identical, working pattern in lfx-mcp. Resolves 4 review threads. Signed-off-by: Eric Searcy --- .github/workflows/build.yaml | 53 ++++++++++++++++++++++++++++++++++++ AGENTS.md | 10 +++++-- Makefile | 9 ++++-- 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..601d9f4 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,53 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +name: Build + +"on": + pull_request: null + +permissions: + contents: read + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + build: + # With GO_GOLANGCI_LINT disabled in MegaLinter, that workflow's + # revive-only run never compiles the Go packages, so this build/vet/test + # job is required to catch code that does not build. + name: Build and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version-file: go.mod + + - name: Format check + # gofmt -l lists unformatted files without rewriting them; fail if + # any are found instead of silently reformatting like `go fmt`. + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "The following files are not gofmt-formatted:" + echo "$unformatted" + exit 1 + fi + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./... + + - name: Test + run: go test ./... diff --git a/AGENTS.md b/AGENTS.md index 56809de..9755211 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,8 +143,14 @@ func NewExampleCommand() *cli.Command { ### Package Comments Every file in a package must start with the same `// Package ...` -doc comment immediately above the `package` declaration (required by -MegaLinter's revive `package-comments` rule). Do not vary the wording +doc comment immediately above the `package` declaration. Revive's +`package-comments` rule itself only requires one such comment per package, +but MegaLinter's `GO_REVIVE` linter defaults to `GO_REVIVE_CLI_LINT_MODE: +list_of_files`, invoking revive with a flat list of files instead of +`./...`. Under that mode revive loses per-package grouping and flags any +file lacking the comment, so duplicating the identical comment across +every file in a package is a required workaround for how MegaLinter calls +revive here, not an inherent revive requirement. Do not vary the wording between files in the same package. ## Documentation Generation diff --git a/Makefile b/Makefile index 6602dfe..b37111f 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,12 @@ CMD_DIR=./cmd/lfx BUILD_DIR=./bin # Build flags. Version is not injected via -ldflags: `go build` already -# embeds VCS info (module pseudo-version, revision, dirty state) into the -# binary's build info, which main.go reads via debug.ReadBuildInfo() as a -# fallback. This keeps `make build` and `go install ...@latest` consistent. +# embeds a pseudo-version (encoding the VCS revision, commit time, and +# dirty state) into the binary's module version, which main.go reads via +# debug.ReadBuildInfo() as a fallback. A local `make build` therefore +# reports a traceable pseudo-version like v0.0.0-