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
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
- name: Check shell script formatting (shfmt)
run: shfmt -d -i 2 -ci -bn -sr scripts/ tests/

- name: Check shell scripts (shellcheck)
run: find scripts tests -name '*.sh' -type f -exec shellcheck --severity=warning {} +

test:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -77,7 +80,7 @@ jobs:
- name: Publish
env:
RID: ${{ matrix.rid }}
run: dotnet publish ./src/Keystone.Cli/Keystone.Cli.csproj -c Release -r "$RID"
run: dotnet publish ./src/Keystone.Cli/Keystone.Cli.csproj -c Release -r "${RID}"

- name: Build .deb package
uses: ./.github/actions/build-deb
Expand Down Expand Up @@ -141,5 +144,5 @@ jobs:
env:
ARCH: ${{ matrix.arch }}
run: |
SAFE_ARCH="$(./scripts/validate-arch.sh "$ARCH")"
bash ./scripts/verify-deb-install.sh ./deb/keystone-cli_*_"$SAFE_ARCH".deb
SAFE_ARCH="$(./scripts/validate-arch.sh "${ARCH}")"
bash ./scripts/verify-deb-install.sh ./deb/keystone-cli_*_"${SAFE_ARCH}".deb
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ jobs:
id: v
shell: bash
run: |
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
echo "version=${GITHUB_REF_NAME#v}" >> "${GITHUB_OUTPUT}"

- name: Validate csproj version matches tag
shell: bash
env:
TAG_VERSION: ${{ steps.v.outputs.version }}
run: |
CS_VERSION="$(./scripts/get-version.sh)"
if [[ "$CS_VERSION" != "$TAG_VERSION" ]]; then
echo "ERROR: csproj <Version> ($CS_VERSION) does not match tag (v$TAG_VERSION)" >&2
if [[ "${CS_VERSION}" != "${TAG_VERSION}" ]]; then
echo "ERROR: csproj <Version> (${CS_VERSION}) does not match tag (v${TAG_VERSION})" >&2
exit 1
fi

Expand Down Expand Up @@ -76,14 +76,14 @@ jobs:
- name: Publish (${{ matrix.rid }})
env:
RID: ${{ matrix.rid }}
run: dotnet publish ./src/Keystone.Cli/Keystone.Cli.csproj -c Release -r "$RID"
run: dotnet publish ./src/Keystone.Cli/Keystone.Cli.csproj -c Release -r "${RID}"

- name: Package tarball (${{ matrix.rid }})
shell: bash
env:
VERSION: ${{ needs.validate.outputs.version }}
RID: ${{ matrix.rid }}
run: bash ./scripts/package-release.sh "$VERSION" "$RID"
run: bash ./scripts/package-release.sh "${VERSION}" "${RID}"

- name: Upload tarball artifact
uses: actions/upload-artifact@v6
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/tag-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Read version from csproj
id: v
shell: bash
run: echo "version=$(./scripts/get-version.sh)" >> "$GITHUB_OUTPUT"
run: echo "version=$(./scripts/get-version.sh)" >> "${GITHUB_OUTPUT}"

- name: Run unit tests (gate tagging)
run: dotnet test ./tests/Keystone.Cli.UnitTests/Keystone.Cli.UnitTests.csproj -c Release
Expand All @@ -42,9 +42,9 @@ jobs:
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
TAG="v$VERSION"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "ERROR: Tag already exists: $TAG" >&2
TAG="v${VERSION}"
if git rev-parse "${TAG}" >/dev/null 2>&1; then
echo "ERROR: Tag already exists: ${TAG}" >&2
exit 1
fi

Expand All @@ -53,13 +53,13 @@ jobs:
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
TAG="v$VERSION"
TAG="v${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "keystone-cli $TAG"
git push origin "$TAG"
git tag -a "${TAG}" -m "keystone-cli ${TAG}"
git push origin "${TAG}"

- name: Summary
env:
VERSION: ${{ steps.v.outputs.version }}
run: echo "Pushed tag v$VERSION. This will trigger the Release workflow."
run: echo "Pushed tag v${VERSION}. This will trigger the Release workflow."
16 changes: 16 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ShellCheck configuration for keystone-cli
# https://www.shellcheck.net/wiki/ShellCheckRC

# Default shell dialect
shell=bash

# Follow source statements to check sourced files
external-sources=true

# Enable stricter optional checks
enable=require-variable-braces
enable=quote-safe-variables
enable=check-unassigned-uppercase
enable=check-extra-masked-returns
enable=require-double-brackets
enable=deprecate-which
35 changes: 27 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ keystone-cli/
│ ├── validate-version.sh # Validate semantic version format
│ └── verify-deb-install.sh # Verify .deb package installation
├── nfpm.yaml # nfpm configuration for .deb packaging
├── .shellcheckrc # ShellCheck configuration
├── Makefile # Development task automation (lint targets)
├── artifacts/ # Build outputs
├── global.json # .NET SDK version (used by local and CI builds)
├── Directory.Build.props # MSBuild configuration (target framework, build settings)
Expand Down Expand Up @@ -105,21 +107,38 @@ The project uses strict code analysis:
- `TreatWarningsAsErrors` is enabled
- Extensive .editorconfig with C# and ReSharper rules
- Uses Microsoft.VisualStudio.Threading.Analyzers
- Shell scripts use shfmt for formatting (`shfmt -i 2 -ci -bn -sr`)
- Shell scripts use shfmt for formatting and shellcheck for static analysis

### Shell Script Formatting
### Shell Script Linting

Scripts in `scripts/` and `tests/deb/` are formatted with shfmt:
Scripts in `scripts/` and `tests/deb/` are checked with shfmt (formatting) and
shellcheck (static analysis).

```bash
# Check formatting (CI uses this)
shfmt -d -i 2 -ci -bn -sr scripts/ tests/
# Run all linters (recommended)
make lint

# Auto-fix formatting issues
make lint-fix

# Format in place (local development)
shfmt -w -i 2 -ci -bn -sr scripts/ tests/
# Individual commands (if not using make)
shfmt -d -i 2 -ci -bn -sr scripts/ tests/
shellcheck --severity=warning scripts/*.sh tests/deb/*.sh
```

Install shfmt: `brew install shfmt` (macOS) or `go install mvdan.cc/sh/v3/cmd/shfmt@latest`
Install tools:

- **shfmt**: `brew install shfmt` (macOS) or `go install mvdan.cc/sh/v3/cmd/shfmt@latest`
- **shellcheck**: `brew install shellcheck` (macOS) or `apt-get install shellcheck` (Debian/Ubuntu)

Configuration:

- shfmt: Flags in Makefile (`-i 2 -ci -bn -sr`)
- shellcheck: `.shellcheckrc` (bash dialect, stricter optional checks enabled)

Shell code in GitHub workflow `run:` blocks should follow the same conventions—use
`${VAR}` (braced) instead of `$VAR` for consistency with shellcheck's
`require-variable-braces` rule.

### Manual Pages

Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ This project follows a structured workflow documented in
- The project uses `TreatWarningsAsErrors` — all warnings must be resolved
- Follow existing code patterns and architecture
- Include unit tests for new functionality
- Shell scripts must pass `shfmt` formatting (see [CLAUDE.md](CLAUDE.md) for details)
- Shell scripts must pass `shfmt` formatting and `shellcheck` analysis
- Run `make lint` to check locally
- Run `make lint-fix` to auto-fix formatting
- Run `dotnet test` before submitting

## Labels
Expand Down
39 changes: 39 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Makefile for keystone-cli development tasks
#
# Usage:
# make lint Check shell script formatting and lint errors
# make lint-fix Auto-fix shell script formatting

.PHONY: help lint lint-fix lint-shfmt lint-shfmt-fix lint-shellcheck

.DEFAULT_GOAL := help

# Shell scripts to lint (all .sh files in scripts/ and tests/)
SHELL_SCRIPTS := $(shell find scripts tests -name '*.sh' -type f)

# shfmt flags (must match ci.yml)
SHFMT_FLAGS := -i 2 -ci -bn -sr

help: ## Show available targets
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'

lint: lint-shfmt lint-shellcheck ## Run all linters (shfmt + shellcheck)
@echo "All checks passed"

lint-fix: lint-shfmt-fix ## Auto-fix formatting issues (shfmt only)
@echo "Note: shellcheck issues must be fixed manually"

lint-shfmt: ## Check shell script formatting
@echo "Checking shell script formatting (shfmt)..."
@shfmt -d $(SHFMT_FLAGS) $(SHELL_SCRIPTS)

lint-shfmt-fix: ## Fix shell script formatting
@echo "Fixing shell script formatting (shfmt)..."
@shfmt -w $(SHFMT_FLAGS) $(SHELL_SCRIPTS)

lint-shellcheck: ## Run shellcheck on shell scripts
@echo "Running shellcheck..."
@shellcheck --severity=warning $(SHELL_SCRIPTS)
6 changes: 6 additions & 0 deletions docs/how-to/how-to-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ set -euo pipefail
| `-u` | Error on unset variables | Catches typos and missing inputs |
| `-o pipefail` | Propagate pipe errors | Catches failures in pipelines |

### Automated Analysis

ShellCheck is integrated into CI to catch common issues automatically. Run `make lint`
locally before committing. The `.shellcheckrc` configuration enables stricter checks
including `require-variable-braces`, `quote-safe-variables`, and `deprecate-which`.

### Input Validation with Allowlists

Validate all external inputs against explicit allowlists. The pattern is:
Expand Down
8 changes: 1 addition & 7 deletions tests/deb/test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,9 @@ HOST_ARCH=$(uname -m)
case "$HOST_ARCH" in
x86_64)
HOST_DEB_ARCH="amd64"
HOST_RID="linux-x64"
;;
aarch64)
aarch64 | arm64)
HOST_DEB_ARCH="arm64"
HOST_RID="linux-arm64"
;;
arm64)
HOST_DEB_ARCH="arm64"
HOST_RID="linux-arm64"
;;
*)
echo "Unknown host architecture: $HOST_ARCH"
Expand Down