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
4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
### Tests Not Performed
<!-- REQUIRED: What tests were not performed? Maybe due to dependencies, or environment? -->

### CI Checks
<!-- CI runs automatically. All checks must pass before merge. -->
- [ ] `make ci` passes locally

### AI Assisted
<!-- REQUIRED -->
- [ ] Yes
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
ci:
name: Build, Test, Lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.24"

- name: Install golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.10
install-mode: binary
args: --help

- name: Run CI
run: make ci
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Compiled binaries
/questcore
/bin/
*.exe
*.dll
*.so
Expand Down
31 changes: 31 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: "2"

run:
timeout: 3m
go: "1.24"

linters:
default: none
enable:
- govet
- errcheck
- staticcheck
- unused
- ineffassign
- gocritic
- misspell

settings:
gocritic:
disabled-checks:
- captLocal # L is conventional in gopher-lua codebases

exclusions:
generated: lax
presets:
- comments
- std-error-handling
rules:
- path: _test\.go
linters:
- errcheck
46 changes: 46 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Makefile for QuestCore
# Single source of truth for build, test, and lint checks.
# Used by both local development and GitHub Actions CI.

BINARY := bin/questcore
GO := go
GOFLAGS := -v
TIMEOUT := 120s

.PHONY: build test lint vet fmt-check fmt ci clean

## build: Compile the questcore binary
build:
$(GO) build $(GOFLAGS) -o $(BINARY) ./cmd/questcore

## test: Run all tests with race detection
test:
$(GO) test $(GOFLAGS) -timeout $(TIMEOUT) -race ./...

## lint: Run golangci-lint
lint:
golangci-lint run

## vet: Run go vet
vet:
$(GO) vet ./...

## fmt-check: Check that all Go files are gofmt-formatted
fmt-check:
@unformatted=$$(gofmt -l .); \
if [ -n "$$unformatted" ]; then \
echo "Unformatted files:"; \
echo "$$unformatted"; \
exit 1; \
fi

## fmt: Format all Go files in place
fmt:
gofmt -w .

## ci: Run the full check pipeline (same as GitHub Actions)
ci: fmt-check vet lint build test

## clean: Remove build artifacts
clean:
rm -rf bin/
51 changes: 51 additions & 0 deletions docs/plans/ci-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# CI/CD Setup — GitHub Actions + Makefile + golangci-lint

## Goal

Add automated build/test/lint gates for pull requests and post-merge validation
on main. Adopt a "local CI" philosophy where the Makefile is the single source
of truth — GitHub Actions calls `make ci`, the same command developers run locally.

## Approach

### Trunk-Based Development

- All development in short-lived feature branches off `main`
- PRs merge to `main` after CI passes
- Post-merge CI on `main` for validation

### Local CI Parity

- `Makefile` defines all check targets: `build`, `test`, `lint`, `vet`, `fmt-check`
- `make ci` runs the full pipeline (fmt-check -> vet -> lint -> build -> test)
- GitHub Actions workflow calls `make ci` — identical to local
- `CI` env var (set automatically by GitHub Actions) available if behavior needs to diverge

### Linting

- golangci-lint v2 with practical linter set: govet, errcheck, staticcheck,
unused, gosimple, ineffassign, typecheck, gocritic, misspell
- errcheck suppressed in test files
- No pedantic style linters (wsl, nlreturn, funlen, etc.)

## Files

| File | Action | Purpose |
|------|--------|---------|
| `.github/workflows/ci.yml` | Create | GitHub Actions workflow calling `make ci` |
| `Makefile` | Create | Build/test/lint targets for local and CI use |
| `.golangci.yml` | Create | golangci-lint v2 configuration |
| `.gitignore` | Update | Add `/bin/` for Makefile build output |
| `.github/pull_request_template.md` | Update | Add CI checklist item |
| 6 Go source files | Update | Fix existing `gofmt` violations |

## Task List

- [x] Create this plan document
- [ ] Create feature branch `ci/github-actions-setup`
- [ ] Fix `gofmt` violations (`gofmt -w .`)
- [ ] Create `.golangci.yml` and fix lint issues
- [ ] Create `Makefile` + update `.gitignore`
- [ ] Create `.github/workflows/ci.yml` + update PR template
- [ ] Verify `make ci` passes locally
- [ ] Push branch and open PR
76 changes: 38 additions & 38 deletions engine/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,44 +110,44 @@ var verbAliases = map[string]string{
"quaff": "drink",

// Miscellaneous
"inv": "inventory",
"i": "inventory",
"z": "wait",
"smell": "smell",
"sniff": "smell",
"listen": "listen",
"hear": "listen",
"touch": "touch",
"feel": "touch",
"rub": "touch",
"climb": "climb",
"scale": "climb",
"jump": "jump",
"leap": "jump",
"hop": "jump",
"unlock": "unlock",
"tie": "tie",
"fasten": "tie",
"attach": "tie",
"untie": "untie",
"detach": "untie",
"release": "untie",
"wear": "wear",
"don": "wear",
"wave": "wave",
"sing": "sing",
"pray": "pray",
"sleep": "sleep",
"nap": "sleep",
"rest": "sleep",
"knock": "knock",
"rap": "knock",
"yell": "yell",
"scream": "yell",
"shout": "yell",
"swim": "swim",
"dive": "swim",
"buy": "buy",
"inv": "inventory",
"i": "inventory",
"z": "wait",
"smell": "smell",
"sniff": "smell",
"listen": "listen",
"hear": "listen",
"touch": "touch",
"feel": "touch",
"rub": "touch",
"climb": "climb",
"scale": "climb",
"jump": "jump",
"leap": "jump",
"hop": "jump",
"unlock": "unlock",
"tie": "tie",
"fasten": "tie",
"attach": "tie",
"untie": "untie",
"detach": "untie",
"release": "untie",
"wear": "wear",
"don": "wear",
"wave": "wave",
"sing": "sing",
"pray": "pray",
"sleep": "sleep",
"nap": "sleep",
"rest": "sleep",
"knock": "knock",
"rap": "knock",
"yell": "yell",
"scream": "yell",
"shout": "yell",
"swim": "swim",
"dive": "swim",
"buy": "buy",
"purchase": "buy",
}

Expand Down
23 changes: 7 additions & 16 deletions loader/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type rawRoom struct {

// rawEntity holds an entity table before compilation.
type rawEntity struct {
id string
kind string
id string
kind string
table *lua.LTable
}

Expand Down Expand Up @@ -49,15 +49,6 @@ func getString(tbl *lua.LTable, key string) string {
return ""
}

// getBool returns a bool field from a Lua table, or the default if missing.
func getBool(tbl *lua.LTable, key string, def bool) bool {
v := tbl.RawGetString(key)
if b, ok := v.(lua.LBool); ok {
return bool(b)
}
return def
}

// getNumber returns a numeric field from a Lua table, or 0 if missing.
func getNumber(tbl *lua.LTable, key string) float64 {
v := tbl.RawGetString(key)
Expand Down Expand Up @@ -258,8 +249,8 @@ func compileRoom(raw rawRoom) (types.RoomDef, []string, error) {
func compileEntity(raw rawEntity) (types.EntityDef, []string, error) {
tbl := raw.table
entity := types.EntityDef{
ID: raw.id,
Kind: raw.kind,
ID: raw.id,
Kind: raw.kind,
Props: map[string]any{},
}

Expand Down Expand Up @@ -349,9 +340,9 @@ func compileRule(raw rawRule) (types.RuleDef, error) {

func compileMatchCriteria(tbl *lua.LTable) types.MatchCriteria {
mc := types.MatchCriteria{
Verb: getString(tbl, "verb"),
Object: getString(tbl, "object"),
Target: getString(tbl, "target"),
Verb: getString(tbl, "verb"),
Object: getString(tbl, "object"),
Target: getString(tbl, "target"),
ObjectKind: getString(tbl, "object_kind"),
}
if tp := getTable(tbl, "target_prop"); tp != nil {
Expand Down
6 changes: 2 additions & 4 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,8 @@ func TestLoad_FullGame(t *testing.T) {
// Handlers.
if len(defs.Handlers) != 1 {
t.Errorf("expected 1 handler, got %d", len(defs.Handlers))
} else {
if defs.Handlers[0].EventType != "door_unlocked" {
t.Errorf("handler event = %q", defs.Handlers[0].EventType)
}
} else if defs.Handlers[0].EventType != "door_unlocked" {
t.Errorf("handler event = %q", defs.Handlers[0].EventType)
}
}

Expand Down
46 changes: 23 additions & 23 deletions loader/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,33 @@ func (e *ValidationError) Error() string {

// Known effect types.
var validEffectTypes = map[string]bool{
"say": true,
"give_item": true,
"remove_item": true,
"set_flag": true,
"inc_counter": true,
"set_counter": true,
"set_prop": true,
"move_entity": true,
"move_player": true,
"open_exit": true,
"close_exit": true,
"emit_event": true,
"start_dialogue": true,
"stop": true,
"say": true,
"give_item": true,
"remove_item": true,
"set_flag": true,
"inc_counter": true,
"set_counter": true,
"set_prop": true,
"move_entity": true,
"move_player": true,
"open_exit": true,
"close_exit": true,
"emit_event": true,
"start_dialogue": true,
"stop": true,
}

// Known condition types.
var validConditionTypes = map[string]bool{
"has_item": true,
"flag_set": true,
"flag_not": true,
"flag_is": true,
"in_room": true,
"prop_is": true,
"counter_gt": true,
"counter_lt": true,
"not": true,
"has_item": true,
"flag_set": true,
"flag_not": true,
"flag_is": true,
"in_room": true,
"prop_is": true,
"counter_gt": true,
"counter_lt": true,
"not": true,
}

// validate checks the compiled defs for referential integrity and consistency.
Expand Down
5 changes: 0 additions & 5 deletions tui/style.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,6 @@ func styledYouSee(line string) string {
return styleRoomDesc.Render(prefix) + styleYouSee.Render(line[len(prefix):])
}

// styledPlayerInput renders the echoed player input in green with "> " prefix.
func styledPlayerInput(input string) string {
return stylePlayerInput.Render("> " + input)
}

// styledSystemMsg renders a system message in gray with brackets.
func styledSystemMsg(text string) string {
return styleSystem.Render("[" + text + "]")
Expand Down
Loading