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
26 changes: 26 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

[{Dockerfile,Dockerfile.*}]
indent_size = 4
tab_width = 4

[{Makefile,makefile,GNUmakefile}]
indent_style = tab
indent_size = 4

[Makefile.*]
indent_style = tab
indent_size = 4

[**/*.{go,mod,sum}]
indent_style = tab
indent_size = unset

[**/*.py]
indent_size = 4
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# debug
APP_DEBUG_ENABLED=false

# githup app (required)
APP_GITHUB_APP_ID=123456
APP_GITHUB_APP_PRIVATE_KEY_PATH=./.local/private-key.pem
APP_GITHUB_INSTALLATION_ID=987654
APP_GITHUB_ORG=cruxstack
APP_GITHUB_WEBHOOK_SECRET=your-webhook-secret-here

# githu pr compliance (optional)
APP_PR_COMPLIANCE_ENABLED=true
APP_PR_MONITORED_BRANCHES=main,master

# okta (optional)
APP_OKTA_DOMAIN=company.okta.com
APP_OKTA_CLIENT_ID=0oaxxxxxxxxxxxxxxxxxxxxx
APP_OKTA_PRIVATE_KEY_PATH=./.local/okta-private-key.pem
# APP_OKTA_SCOPES=okta.groups.read,okta.users.read

# okta sync rules
APP_OKTA_GITHUB_USER_FIELD=githubUsername
APP_OKTA_SYNC_RULES=[{"name":"sync-eng","enabled":true,"okta_group_pattern":"^github-eng-.*","github_team_prefix":"eng-","strip_prefix":"github-eng-","sync_members":true,"create_team_if_missing":true}]
# APP_OKTA_SYNC_SAFETY_THRESHOLD=0.5 # Prevent mass removal if more than 50% would be removed (default: 0.5)

# slack configuration (optional)
APP_SLACK_TOKEN=xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_SLACK_CHANNEL=C01234ABCDE
6 changes: 6 additions & 0 deletions .github/.dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
67 changes: 67 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: ci

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Run go vet
run: go vet ./...

- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "The following files are not formatted:"
echo "$unformatted"
exit 1
fi

test:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Run tests
run: make test

- name: Run tests
run: make test-verify-verbose

build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Build Lambda
run: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -C cmd/lambda -o ../../dist/bootstrap
29 changes: 29 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: release

on:
push:
branches:
- main

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Bump Version
id: tag_version
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
default_bump: minor
custom_release_rules: bug:patch:Fixes,chore:patch:Chores,docs:patch:Documentation,feat:minor:Features,refactor:minor:Refactors,test:patch:Tests,ci:patch:Development,dev:patch:Development
- name: Create Release
uses: ncipollo/release-action@v1.12.0
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
body: ${{ steps.tag_version.outputs.changelog }}
26 changes: 26 additions & 0 deletions .github/workflows/semantic-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: semantic-check
on:
pull_request_target:
types:
- opened
- edited
- synchronize

permissions:
contents: read
pull-requests: read

jobs:
main:
name: Semantic Commit Message Check
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- uses: amannn/action-semantic-pull-request@v5.2.0
name: Check PR for Semantic Commit Message
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
requireScope: false
validateSingleCommit: true
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
!**/.gitkeep

tmp/
dist/
.DS_Store

.local/
.env

cognito-hooks-go
main

123 changes: 123 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Agent Guidelines

## Architecture Overview
- **Deployment**: Standard HTTP server OR AWS Lambda (both supported)
- **GitHub**: GitHub App required (JWT + installation token authentication)
- **Features**: Okta group sync + PR compliance monitoring (both optional)
- **Entry points**:
- `cmd/server/main.go` - Standard HTTP server (VPS, container, K8s)
- `cmd/lambda/main.go` - Lambda adapter (API Gateway + EventBridge)
- `cmd/verify/main.go` - Integration tests with HTTP mock servers
- `cmd/sample/main.go` - **DO NOT RUN** (requires live credentials)
- **Packages**:
- `internal/app/` - Core logic, configuration, and HTTP handlers (no AWS
dependencies)
- `internal/github/` - API client, webhooks, PR checks, team mgmt, auth
- `internal/okta/` - API client, group sync
- `internal/notifiers/` - Slack formatting for events and reports
- `internal/errors/` - Sentinel errors

## Build & Test
- **Build server**: `make build-server` (creates `dist/server`)
- **Build Lambda**: `make build-lambda` (creates `dist/bootstrap`)
- **Run server locally**: `make server`
- **Test all**: `make test` (runs with `-race -count=1`)
- **Test single package**: `go test -race -count=1 ./internal/github`
- **Test single function**: `go test -race -count=1 ./internal/okta -run
TestGroupSync`
- **Integration tests**: `make test-verify` (offline tests using HTTP mock
servers)
- **Verbose integration tests**: `make test-verify-verbose` (shows all HTTP
requests during testing)
- **Lint**: `go vet ./...` and `gofmt -l .`

IMPORTANT: DO NOT run `go run cmd/sample/main.go` as it requires live
credentials and makes real API calls to GitHub/Okta/Slack.

## Code Style
- **Imports**: stdlib, blank line, third-party, local (e.g., `internal/`)
- **Naming**: `PascalCase` exports, `camelCase` private, `ALL_CAPS` env vars (prefixed `APP_`)
- **Structs**: define types in package; constructors as `New()` or `NewTypeName()`; methods public (PascalCase)
- **Formatting**: `gofmt` (tabs for indentation)
- **Comments**: rare, lowercase, short; prefer self-documenting code
- **Error handling**: return errors up stack; wrap with `fmt.Errorf` (see Error Handling below)

## Error Handling

### Error Message Format
- **Style**: lowercase, action-focused, concise
- **Pattern**: `"failed to {action} {object}: {context}"`
- **Always include**: specific identifiers (PR numbers, team names, IDs)
- **Examples**:
- ✅ `"failed to fetch pr #123 from owner/repo: %w"`
- ✅ `"failed to create team 'engineers' in org 'myorg': %w"`
- ✅ `"required check 'ci' did not pass"`
- ❌ `"PR is nil"` (no context)
- ❌ `"Failed to Get Team"` (capitalized, generic)

### Error Wrapping
- Use `github.com/cockroachdb/errors` package for all error handling
- **Wrap errors**: `errors.Wrap(err, "context")` or `errors.Wrapf(err,
"failed to sync team '%s'", teamName)`
- **Create new errors**: `errors.New("message")` or `errors.Newf("error:
%s", context)`
- Automatically captures stack traces for debugging Lambda issues
- Preserve original error context while adding specific details

### Sentinel Errors
- Define common errors in `internal/errors/errors.go`
- Each sentinel error is marked with a domain type (ValidationError,
AuthError, APIError, ConfigError)
- Domain markers enable error classification and monitoring
- Use `errors.Is()` to check for sentinel errors in tests
- Use `errors.HasType()` to check for error domains
- Examples: `ErrMissingPRData`, `ErrInvalidSignature`, `ErrClientNotInit`

### Stack Traces
- Automatically captured when wrapping errors with cockroachdb/errors
- No performance overhead unless error is formatted
- Critical for debugging serverless Lambda executions
- Use `errors.WithDetailf()` to add structured context to auth/API errors

### Validation
- Validate at parse time, not during processing
- Webhook events validated in `ParseXxxEvent()` functions
- Return detailed validation errors immediately
- Prevents nil pointer issues downstream

### Error Logging vs Returning
- **Fatal errors** (config, init): return immediately
- **Recoverable errors** (individual items in batch): collect and continue
- **Optional features** (notifications): log only, don't fail parent operation
- Lambda handlers: log detailed errors, return sanitized messages to client

### Batch Operation Errors
- Collect errors in result structs (e.g., `SyncReport.Errors`)
- Continue processing remaining items
- Return aggregated results with partial success
- Helper methods: `HasErrors()`, `HasChanges()`

## Authentication & Integration

### GitHub
- **Required**: GitHub App (JWT + installation tokens, automatic rotation)
- **Auth flow**: JWT signed with private key → exchange for installation token → cached with auto-refresh
- **Webhooks**: HMAC-SHA256 signature verification

### Okta
- OAuth 2.0 with private key authentication
- **Required scopes**: `okta.groups.read` and `okta.users.read`
- Sync uses slug-based GitHub Teams API

### Slack
- Optional notifications for PR events and sync reports
- Configuration in `internal/notifiers/`

## Markdown Style
- **Line length**: Max 80 characters for prose text
- **Exceptions**: Code blocks, links, ASCII art, tables
- **Table alignment**: Align columns in plaintext for readability
- **Wrapping**: Break at natural boundaries (commas, periods, conjunctions)
- **Lists**: Indent continuation lines with 2 spaces

NOTE: AGENTS.md itself is exempt from the 80-character line limit.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 CruxStack

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
45 changes: 45 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# set mac-only linker flags only for go test (not global)
UNAME_S := $(shell uname -s)
TEST_ENV :=
ifeq ($(UNAME_S),Darwin)
TEST_ENV = CGO_LDFLAGS=-w
endif

TEST_FLAGS := -race -count=1

.PHONY: build-lambda
build-lambda:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o dist/bootstrap ./cmd/lambda

.PHONY: build-server
build-server:
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o dist/server ./cmd/server

.PHONY: build-debug
build-debug:
GOOS=$(GOOS) GOARCH=$(GOARCH) go build -trimpath -ldflags "-s -w" -o dist/sample ./cmd/sample

.PHONY: debug
debug:
go run ./cmd/sample

.PHONY: server
server:
go run ./cmd/server

.PHONY: test
test:
$(TEST_ENV) go test $(TEST_FLAGS) ./...

.PHONY: test-unit
test-unit:
$(TEST_ENV) go test $(TEST_FLAGS) ./internal/...

.PHONY: test-verify
test-verify:
go run ./cmd/verify

.PHONY: test-verify-verbose
test-verify-verbose:
go run ./cmd/verify -verbose

Loading