Skip to content

Latest commit

 

History

History
353 lines (277 loc) · 21.3 KB

CONTRIBUTING.md

File metadata and controls

353 lines (277 loc) · 21.3 KB

Contributing to Floci

Thank you for your interest in contributing! Floci is a community-driven project and all contributions are welcome.

Join us on Slack: it is the fastest way to reach maintainers. Ask about AWS behaviour, sanity-check an approach before you build it, or get unstuck on a PR.

Ways to Contribute

  • Bug reports: open an issue with a minimal reproduction
  • Feature requests: open an issue describing the AWS behavior you need
  • Pull requests: bug fixes, new service implementations, or improvements
  • Compatibility tests: add cases to ./compatibility-tests/

Getting Started

Prerequisites

  • Java 25+
  • Maven 3.9+
  • Docker (for integration tests that spin up Lambda/RDS/ElastiCache)

Any Java 25+ distribution will work. If you need to install it, SDKMAN is a convenient option:

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 25-open

Build & Run

This project includes a Maven wrapper, so you don't need to install Maven separately:

git clone https://github.com/floci-io/floci.git
cd floci
./mvnw quarkus:dev     # hot reload on port 4566

If you prefer to use your own Maven installation (3.9+), you can use mvn instead of ./mvnw.

make help lists the repository's shortcuts: make dev, make test T=SsmIntegrationTest, make native and make native-image for the native binary and its Docker image with the flags CI uses, make native-host and make run-native for a binary built and run on your machine without Docker, make native-up to run the image, and make compat SUITES="sdk-test-java" to run compatibility suites in Docker against it.

Run Tests

./mvnw test                                          # all tests
./mvnw test -Dtest=SsmIntegrationTest                # single class
./mvnw test -Dtest=SsmIntegrationTest#putParameter   # single method

Branching Model

Floci uses a tag-driven release model. Merging a PR never publishes a release image. main is published once a day as the nightly channel, and a stable release is published only when a maintainer runs the "Release Cut" workflow (.github/workflows/release-cut.yml, workflow_dispatch). That workflow runs semantic-release, which bumps pom.xml, writes CHANGELOG.md, commits, and pushes the X.Y.Z tag. The tag then triggers .github/workflows/release.yml, which builds and publishes the release images to Docker Hub and public ECR.

Branch / tag Purpose Docker published?
main Integration branch: all PRs merge here. Treated as unstable. Nightly only (nightly, nightly-<date>, nightly-compat, nightly-<date>-compat) via nightly.yml
X.Y.Z tag Signals a production release. Pushed by the Release Cut workflow. Yes (x.y.z, latest, x.y.z-compat, latest-compat) via release.yml

Commit Message Format

This project uses Conventional Commits: semantic-release reads these to generate the changelog and version bumps automatically.

The PR title is validated automatically by CI and must follow this format, since it becomes the squash-merge commit message that semantic-release reads.

Format

<type>[optional scope]: <description>
  • type: one of the values in the table below (lowercase)
  • scope: optional, in parentheses, identifies the service or area (e.g. s3, dynamodb, core)
  • description: short summary in the imperative mood, no trailing period
  • Append ! before the colon to signal a breaking change: feat(api)!:
Type When to use Version bump
feat New AWS API action or service minor
fix Bug fix or AWS compatibility correction patch
perf Performance improvement patch
revert Reverts a previous commit patch
docs Documentation only none
style Formatting, whitespace, no logic change none
chore Build, CI, dependencies, housekeeping none
refactor Code restructure without behavior change none
test Adding or updating tests none
build Build system or tooling changes none
ci CI workflow changes none
BREAKING CHANGE Footer or ! suffix, incompatible change major

Valid examples ✅

feat(dynamodb): add PartiQL ExecuteStatement support
fix(s3): make us-east-1 bucket creation idempotent
perf(kinesis): reduce lock contention in shard iterator
chore: release 1.5.16
docs: update README with new configuration options
refactor(sqs): extract message visibility logic
test(kms): add encrypt/decrypt round-trip test
feat!: remove legacy v1 endpoint
fix(dynamodb)!: correct TransactWriteItems error shape
ci: add conventional commits lint workflow
build: bump Quarkus to 3.32.3

Invalid examples ❌

Add PartiQL support                  # missing type
Feature: add something               # "Feature" is not a valid type
feat : space before colon            # space before colon
feat(dynamodb)add missing colon      # missing colon
FIX(s3): uppercase type              # type must be lowercase
feat(my scope): scope has spaces     # scope cannot contain spaces
fix(): empty scope                   # empty scope
feat(s3):no space after colon        # missing space after colon
wip: still working on this          # "wip" is not a recognised type

Do not include Co-Authored-By trailers for AI tools in commit messages. Attribution should be limited to human contributors.

CI enforces this: the Commits omit AI attribution trailers step of the PR hygiene check fails a pull request whose commits carry a Co-Authored-By for an identity that is not a person (one whose GitHub address belongs to a bot account, or that uses a noreply@ mailbox), along with the session and generator lines such tools add on their own. It never judges a co-author by name, so co-authoring a person is always fine; that trailer is how GitHub credits reviewers on a squash merge. dependabot[bot] is allowlisted. If the check fires, drop the offending lines with git commit --amend (or a rebase for several commits) and force-push.

Architecture

See AGENTS.md for a detailed description of the three-layer architecture (Controller → Service → Storage), the AWS wire protocol mapping, and conventions for adding new services.

AGENTS.md is the canonical agent instructions file for this repository, following the AGENTS.md standard. If your coding agent expects a different filename, create a local symlink to AGENTS.md instead of copying the file.

ln -s AGENTS.md CLAUDE.md
ln -s AGENTS.md GEMINI.md
ln -s AGENTS.md COPILOT.md

Code Style

AGENTS.md carries the full list. The rules worth knowing before your first PR:

  • Pass Checkstyle. Run ./mvnw checkstyle:check. The rules live in tools/checkstyle/checkstyle.xml.
  • Write explicit types. Do not use var. Floci reproduces AWS wire contracts, so the concrete type at a call site is usually what a reviewer needs to see: whether a value is a LinkedHashMap or a Map, an AWS model type or a JDK one. The one exception is a record deconstruction pattern.
  • Import the classes you use. No fully-qualified names inline. new ArrayList<>(), never new java.util.ArrayList<>(). Qualify inline only for a genuine name collision in that file, and say in a comment what collides.
  • No wildcard imports in src/main. Static wildcards are fine in tests.
  • Never leave a catch block empty. If swallowing is correct, name the variable ignored or expected and say why in a comment.
  • Always use braces in conditionals, and use constructor injection.
  • Tests: JUnit 5 with Hamcrest and RestAssured. Name methods as a camelCase sentence or method_scenario_expectation, never testX.

Existing code does not yet satisfy all of these everywhere. Match the rules in code you add or change; leave unrelated cleanups for their own PR.

Adding a New AWS Service

  1. Create a package under src/main/java/.../services/<service>/
  2. Add a Controller (follow the correct protocol: Query, JSON 1.1, REST JSON, or REST XML)
  3. Add a Service (@ApplicationScoped) and model POJOs
  4. Add a <Svc>ServiceConfig interface and its accessor on ServicesConfig in EmulatorConfig.java
  5. Register a ServiceDescriptor in ResolvedServiceCatalog. This is the only registration point: ServiceRegistry reads the catalog and has no registration API
  6. Add floci.services.<key>.enabled to both src/main/resources/application.yml and src/test/resources/application.yml
  7. Wire controller/handler dispatch for the service (JSON 1.1 handlers are injected into AwsJson11Controller)
  8. Obtain storage through StorageFactory and implement Resettable; list any static Random or SecureRandom field under --initialize-at-run-time in application.yml
  9. Check every timestamp member you emit for a TimestampFormatTrait before reaching for the epoch-seconds idiom. It is the awsJson1.1 default, but a model can override it per member, and a mismatch is invisible to the AWS CLI because botocore coerces the value, while strict SDKs (Go, Java) reject the whole response. javap -c on the SDK model class shows the traits attached to each SdkField. In Redshift Serverless, for instance, Namespace.creationDate is ISO_8601 while roughly half the model's other timestamp members carry no trait and use the epoch default. The trait is per member, so check each one you emit
  10. Add *ServiceTest.java and *IntegrationTest.java tests
  11. Document it: docs/services/<service>.md, a mkdocs.yml nav entry, a Service Matrix row in docs/services/index.md, and a row in the README category table
  12. Register the handler in tools/docs/services.yaml, then run make docs-sync and make docs-check
  13. Add a client factory in the compat suite's TestFixtures and a <Svc>Test under compatibility-tests/sdk-test-java

ServiceRegistry, ServiceEnabledFilter, and StorageFactory resolve service metadata from the descriptor catalog. Adding a service should not require new service-keyed switch statements in those consumers. make docs-check gates steps 11 and 12: a registered service with no matrix row, a docs/services page with no matrix row, and a stale action table all fail CI.

Always implement the real AWS wire protocol. Never invent custom endpoints. The AWS SDK must work against Floci without modification.

Adding a CloudFormation Resource Type

CloudFormation resource types live in per-service provisioner classes under services/cloudformation/provisioners/; CfnResourceDispatcher only routes a resource to the registry and stubs what nothing serves, so please do not add type-specific code to it. If the service you need already has a *CfnProvisioner, add your type there; otherwise create one.

  1. Create services/cloudformation/provisioners/<Service>CfnProvisioner.java, annotate it @ApplicationScoped, and inject only the service it wraps. Registration is automatic: CloudFormationResourceRegistry discovers it via CDI. (Forgetting @ApplicationScoped compiles and unit-tests green, but the type is never wired.)
  2. Implement resourceTypes() returning every AWS::* type the class serves, and provision(...). When you serve more than one type, switch on resource.getResourceType().
  3. In provision, set both:
    • resource.setPhysicalId(...): this is what Ref resolves to
    • resource.getAttributes().put("Name", value) for every Fn::GetAtt attribute, a separate map. Forgetting it does not fail: Fn::GetAtt silently resolves to the literal string "LogicalId.Attr". Take the attribute names from the resource's registry schema under local/aws/cfn-resource-schemas/us-east-1/ (readOnlyProperties).
  4. provision is also the update path. On UpdateStack it is called again with the previous physical id and attributes already set on the resource. Use ctx.isUpdate() and ctx.priorPhysicalId() rather than reading the id back off the resource: provision assigns the new id as it works, so a resource-derived check changes meaning mid-method. Update in place rather than creating unconditionally, which would otherwise throw AlreadyExists or orphan the old resource.
  5. Override delete(...) if the type has a backing delete. Deleting a resource that is already gone should be tolerated: CfnDeletes.safeDelete does this, taking the specific error codes that mean "already gone" (never a catch-all: a real failure such as BucketNotEmpty must propagate so the stack reports DELETE_FAILED). If the delete needs more than the physical id (a create-time attribute such as a rule's event bus), override delete(StackResource, String) instead, which receives the whole resource.
  6. Register the type in src/test/resources/cloudformation/supported-resource-types.tsv (type<TAB>YourCfnProvisioner). CfnResourceInventoryTest compares that file against the CDI-resolved registry, so it fails if you forget, which is also what catches a missing @ApplicationScoped.
  7. Make your provisioner reachable from CfnProvisionerFixture if it takes a single service. That is three edits, not one: a private <Svc>Service field on Builder, a public Builder <svc>(<Svc>Service v) setter for it, and a construction arm in inferredProvisioners(). Tests name a service and the fixture wires the matching provisioner; without all three, a test exercising your type silently falls through to the stub arm instead. The setter is the step that gets missed: a field nothing can assign stays null, so its construction arm never runs. CfnProvisionerFixtureTest fails if the provisioner cannot be reached through the public builder.
  8. Add a focused unit test (mock only your service, see SqsCfnProvisioner's test) and an integration test. Assert the specific Fn::GetAtt attribute keys, not just CREATE_COMPLETE: an unmapped type is stubbed out as a successful no-op, so a status-only assertion passes even when nothing was provisioned.
  9. Run make docs-sync to regenerate the resource-type table in docs/services/cloudformation.md, and commit the result. Do not hand-edit that table: it is generated from the inventory in step 6. Presentation (service label, ordering, notes) lives in tools/docs/cfn_resource_types.yaml.
  10. If your type has a schema readOnlyProperties entry you cannot set, add a row to src/test/resources/cloudformation/getatt-attribute-gaps.tsv with the reason. CfnSchemaCoverageTest requires every unset attribute to be either fixed or recorded, so "not emulated" stays distinguishable from "forgotten".

SqsCfnProvisioner is the smallest reference implementation; Ec2LaunchTemplateCfnProvisioner shows update-in-place and replacement handling; LogsCfnProvisioner shows a full reconcile-vs-replace update path.

Pull Request Guidelines

  1. Branch off main: git checkout -b feature/my-feature
  2. Open a PR targeting main.
  3. CI runs tests automatically. All checks must pass before merge.
  4. Keep PRs focused: one feature or fix per PR.
  5. Reference any related issues in the PR description.
  6. Keep at most 2 open, non-draft PRs at a time: see Pull Request Limits and Review Bandwidth.

Docker images are built on contributor PRs only for the compatibility suite (.github/workflows/compatibility.yml, on changes under src/** and the compat test trees) and are never published, so merging to main is always cheap.

Pull Request Limits and Review Bandwidth

To make sure every contribution gets a thorough, high-quality review in a reasonable time, we ask contributors to keep no more than 2 open, non-draft pull requests at any time in this repository.

  • Why this policy exists: maintainer review time is limited. Capping concurrent open PRs prevents review backlogs, reduces context switching, and keeps PR cycle times short for everyone.
  • Dependent work: if your work depends on a PR that has not been merged yet, build on that branch or note the dependency in the discussion instead of opening separate, uncoordinated PRs.
  • Draft PRs: drafts do not count against the limit. Mark a draft as ready for review only when you have review capacity available.
  • How it is applied: a bot labels your 3rd and later open pull requests over-pr-limit with a reminder. Starting 2026-10-08, from your 5th open pull request onward, new ones are closed automatically. Your branch and commits are kept, and you can reopen the PR once one of your other PRs is merged or closed. Maintainers and dependency bots are not counted.

Once your current pull requests are reviewed, merged, or closed, you are welcome to open new ones!

Release Process (maintainers)

Stable releases ship on the 1st and 3rd Tuesday of each month. Merging to main does not cut a release: the change rides the next train, and reaches the nightly image on the next nightly build.

Releases are cut from main with the Release Cut workflow (Actions → Release Cut → Run workflow). semantic-release analyzes the Conventional Commits since the last tag, bumps pom.xml, regenerates CHANGELOG.md, commits, tags, and publishes the GitHub Release; the tag push triggers the Docker publish pipeline. Use the dry-run input to preview the next version and notes without releasing.

CHANGELOG.md is generated. Do not edit it by hand. Your Conventional Commit message is the changelog entry. Genuine corrections to the file require the changelog-edit label on the PR.

Testing Policy for Pull Requests

Floci accepts pull requests only when the test coverage is appropriate for the type of change being proposed.

As a project policy:

  • Pull requests that introduce new behavior must include tests that validate that behavior.
  • Pull requests that fix bugs should include a regression test whenever the bug can be covered realistically.
  • Pull requests that modify runtime logic, request handling, persistence behavior, protocol compatibility, or service responses are expected to include updated or additional tests.
  • Pull requests that do not change observable behavior, such as documentation updates, formatting, comments, dependency housekeeping, or low-risk internal refactors, may not require new tests.
  • Even when no new tests are needed, the existing test suite must still pass.

If a pull request does not include new tests, the author should explain why in the PR description. Valid reasons may include:

  • no functional behavior changed
  • existing tests already cover the change
  • the change is not meaningfully testable in isolation

Maintainers may request additional or more targeted test coverage before approving a PR.

CI runs automatically on every pull request, and build/test checks must pass before merge.

Documentation: Action Tables

The Supported Actions tables in docs/services/*.md are generated from handler source, so they cannot drift from the code. The action list comes from the handler (case "X" -> arms or REST controller methods); the Description column is hand-written and preserved across regeneration, keyed by action name.

  • Do not hand-edit the action rows between the <!-- floci:actions:start --> and <!-- floci:actions:end --> markers. Edit the handler, then regenerate.
  • After adding or changing a handler action, run make docs-sync and commit the updated doc alongside your code. Fill in the - placeholder description for any new row.
  • make docs-check (run in CI) fails if a registered service's table is out of date, or if a new switch handler is neither registered in tools/docs/services.yaml nor listed under deferred_handlers.
  • make docs-test runs the tooling's unit tests.

Registering a new service's action table is one entry in tools/docs/services.yaml.

Partition literals

Floci serves every AWS partition, so src/main/java must not bake the commercial one in. make partition-check (run in CI) inventories arn:aws: prefixes, amazonaws.com hosts, Route 53 hosted-zone ids and hand-rolled partition regexes per file against tools/partition/baseline.tsv: a literal in a new file fails, growth fails, and a drop fails until you run make partition-baseline and commit the smaller baseline. make partition-audit prints what is left per package; make partition-test runs the tooling's unit tests. Mint ARNs through AwsArnUtils and derive hosts from the region's DNS suffix; a literal that really is partition-invariant goes in tools/partition/allowlist.yaml with a reason, or ends its line with // partition-literal: <reason>.

The partition catalog itself (src/main/resources/aws/partitions.json: ids, DNS suffixes, regions, opt-in flags, global endpoints) is generated from botocore's published data by make aws-data-sync and checked by make aws-data-check in CI; never hand-edit it. Bump tools/aws/requirements.txt when regenerating from a newer botocore.

Reporting Security Issues

Please do not open public issues for security vulnerabilities. Report them privately by emailing the maintainer or using GitHub private vulnerability reporting.