Skip to content

feat(self-packaging #47): 12-factor env vars FLAPI_CONFIG + FLAPI_LOG_LEVEL#57

Merged
jrosskopf merged 1 commit into
mainfrom
feature/gh-47-env-config-loglevel
May 22, 2026
Merged

feat(self-packaging #47): 12-factor env vars FLAPI_CONFIG + FLAPI_LOG_LEVEL#57
jrosskopf merged 1 commit into
mainfrom
feature/gh-47-env-config-loglevel

Conversation

@jrosskopf
Copy link
Copy Markdown
Contributor

Part of epic #40. Stacked on #56 -> #55 -> #54 -> #53 -> #52 -> #51.

Summary

A bundled flapi binary should be configurable purely via environment
variables (12-factor app). This PR closes two known gaps:

  • FLAPI_CONFIG falls back for -c / --config. The CLAUDE.md docs
    already mentioned it as if it worked; this PR actually wires it.
  • FLAPI_LOG_LEVEL falls back for --log-level. Invalid values cause
    flapi to exit with a clear single-line error rather than silently
    coercing -- typos like FLAPI_LOG_LEVEL=DEBUG (caps) surface
    immediately.

Precedence

CLI flag  >  env var  >  built-in default

Enforced via argparse's is_used() query -- we only consult getenv
when the user didn't pass the flag.

Implementation (src/main.cpp, 24 lines)

if (!program.is_used(\"--config\")) {
    if (const char* env = std::getenv(\"FLAPI_CONFIG\"); env && *env) {
        config_file = env;
    }
}
if (!program.is_used(\"--log-level\")) {
    if (const char* env = std::getenv(\"FLAPI_LOG_LEVEL\"); env && *env) {
        log_level = env;
    }
}
if (log_level != \"debug\" && log_level != \"info\" &&
    log_level != \"warning\" && log_level != \"error\") {
    std::cerr << \"flapi: invalid log level '\" << log_level
              << \"'; must be one of: debug, info, warning, error\\n\";
    return 1;
}

Integration tests (test/integration/test_env_overrides.py, 6 cases, ~4 sec)

# Test What it asserts
1 invalid FLAPI_LOG_LEVEL exit 1; error names the offending value
2 FLAPI_LOG_LEVEL=debug DEBUG log lines appear at validate-config
3 CLI --log-level error + env debug no DEBUG lines (CLI wins)
4 FLAPI_CONFIG=<path> no -c env path is loaded
5 -c <valid> + FLAPI_CONFIG=<broken> CLI wins; validate succeeds
6 neither flag nor env, default cwd flapi.yaml in cwd is loaded
============================== 6 passed in 4.25s ===============================

Docs

  • docs/CLI_REFERENCE.md: env-var rows + precedence lists under
    --config and --log-level, plus example shell invocations.
  • docs/CONFIG_REFERENCE.md: new "12-factor checklist" subsection
    consolidating every env var flapi reads (startup vs query time),
    with a pointer to the flapi pack secret deny list as the
    packaging-time enforcement of "secrets from env, never from bundle".

Out of scope (deferred -- user explicitly narrowed #47 to these two vars)

  • FLAPI_PORT / FLAPI_HOST
  • Full credential env-var audit / inventory

Test plan

  • 6 new integration tests pass on Linux x86_64 debug.
  • Existing 8 self-packaging tests still pass (the precedence
    logic is in a separate code path from pack / info / unpack).
  • Smoke: FLAPI_LOG_LEVEL=verbose flapi --validate-config -c examples/flapi.yaml exits 1 with the documented error message.
  • CI cross-platform once the whole stack lands.

Closes #47. Part of #40.

Part of #40. Closes #47.

A bundled flapi binary should be operable purely via environment
variables (12-factor app). Two known gaps closed:

- `FLAPI_CONFIG` falls back for `-c` / `--config`. The CLAUDE.md
  docs already mentioned it as if it worked; this PR actually wires
  it.
- `FLAPI_LOG_LEVEL` falls back for `--log-level`. Invalid values
  cause flapi to exit with a single-line error message rather than
  silently coercing to `info` -- typos like FLAPI_LOG_LEVEL=DEBUG
  surface immediately.

Precedence (highest wins): CLI flag > env var > built-in default.
The CLI-wins rule is enforced via argparse's `is_used()` query --
we only consult the env when the user didn't pass the flag.

Implementation (src/main.cpp): 24 lines after parse_args; checks
`is_used("--config")` / `is_used("--log-level")`, reads `getenv()`
on miss, validates log_level against the allowed enum.

Integration tests (test/integration/test_env_overrides.py, 6 cases,
~4 sec on Linux x86_64 debug):
- invalid FLAPI_LOG_LEVEL -> exit 1 with "invalid log level" + offending value
- FLAPI_LOG_LEVEL=debug -> DEBUG log lines appear at validate-config
- CLI --log-level wins over FLAPI_LOG_LEVEL
- FLAPI_CONFIG used when no -c flag
- CLI -c wins over FLAPI_CONFIG (env points at broken YAML, validate
  still succeeds against the CLI-supplied path)
- default lookup of `flapi.yaml` in cwd still works when neither set

Docs:
- docs/CLI_REFERENCE.md: env-var rows + precedence list under
  `--config` and `--log-level`, plus example invocations.
- docs/CONFIG_REFERENCE.md: new "12-factor checklist" section
  consolidating every env var flapi reads (startup vs query time),
  with a note that the `flapi pack` secret deny list enforces the
  "secrets from env, never from bundle" rule at packaging time.

Out of scope (deferred): FLAPI_PORT / FLAPI_HOST and a full
credential env-var audit (the user explicitly narrowed #47 to these
two vars in the planning round).
@jrosskopf jrosskopf force-pushed the feature/gh-47-env-config-loglevel branch from 1e73113 to 817ce25 Compare May 22, 2026 16:10
@jrosskopf jrosskopf marked this pull request as ready for review May 22, 2026 16:58
@jrosskopf jrosskopf merged commit c40b083 into main May 22, 2026
17 checks passed
@jrosskopf jrosskopf deleted the feature/gh-47-env-config-loglevel branch May 22, 2026 16:58
jrosskopf added a commit that referenced this pull request May 22, 2026
Part of #40. Closes #49.

Adds four new CI jobs that exercise the self-packaging surface on
every platform flapi builds for:

  pack-smoke-linux-amd64   ubuntu-24.04
  pack-smoke-linux-arm64   ubuntu-24.04-arm
  pack-smoke-macos         macos-latest
  pack-smoke-windows       windows-latest

Each job:
- downloads the platform's `flapi` artifact from the existing
  windows-build / linux-build / osx-universal-build jobs
- builds a tiny fixture tree (flapi.yaml + one endpoint + sample SQL)
- runs `flapi pack --in fixture --out out-a`
- runs `out-a info`, asserts the entry list contains the fixture files
- runs `out-a unpack --to extracted`, diffs files byte-for-byte
- runs a second `flapi pack ... --out out-b` with the same
  `SOURCE_DATE_EPOCH=1700000000` and asserts
  `sha256(out-a) == sha256(out-b)` -- the reproducible-build invariant
  baked into archive_io (#41)

The macOS leg additionally:
- runs `otool -l` to confirm the unbundled binary carries the
  reserved `__FLAPI/__bundle` segment from link time
- runs `codesign --verify --strict out-segment` after the default
  reserved-segment pack -- the notarisation precondition this whole
  approach was built for
- runs `flapi pack --macos-append --out out-append` and confirms
  `info` still discovers the bundle (ad-hoc legacy path still works,
  signature is intentionally invalid -- we do not run codesign verify
  here)
- runs an oversized-payload pack (32 MiB into a 16-MiB segment) and
  confirms it exits non-zero with both "FLAPI_RESERVED_BUNDLE_MIB"
  and "reserved|exceeds" in the error message

The Windows job uses pwsh; pack/info/unpack/sha256 logic mirrors
Linux via PowerShell idioms.

`create-release` now `needs` the four new jobs in addition to the
existing build + smoke matrix, so a broken pack on any platform
blocks the release.

The existing `integration-tests` job (which already runs on
ubuntu-24.04 amd64) auto-picks up `test_self_packaging.py` (#56) and
`test_env_overrides.py` (#57) via pytest discovery -- no change
needed there.

Note: on macOS we deliberately do NOT assert reproducibility because
the codesign step may include non-deterministic data; on Linux and
Windows the host-bytes + appended-archive layout is reproducible by
construction.
jrosskopf added a commit that referenced this pull request May 22, 2026
* ci: cross-platform self-packaging smoke jobs

Part of #40. Closes #49.

Adds four new CI jobs that exercise the self-packaging surface on
every platform flapi builds for:

  pack-smoke-linux-amd64   ubuntu-24.04
  pack-smoke-linux-arm64   ubuntu-24.04-arm
  pack-smoke-macos         macos-latest
  pack-smoke-windows       windows-latest

Each job:
- downloads the platform's `flapi` artifact from the existing
  windows-build / linux-build / osx-universal-build jobs
- builds a tiny fixture tree (flapi.yaml + one endpoint + sample SQL)
- runs `flapi pack --in fixture --out out-a`
- runs `out-a info`, asserts the entry list contains the fixture files
- runs `out-a unpack --to extracted`, diffs files byte-for-byte
- runs a second `flapi pack ... --out out-b` with the same
  `SOURCE_DATE_EPOCH=1700000000` and asserts
  `sha256(out-a) == sha256(out-b)` -- the reproducible-build invariant
  baked into archive_io (#41)

The macOS leg additionally:
- runs `otool -l` to confirm the unbundled binary carries the
  reserved `__FLAPI/__bundle` segment from link time
- runs `codesign --verify --strict out-segment` after the default
  reserved-segment pack -- the notarisation precondition this whole
  approach was built for
- runs `flapi pack --macos-append --out out-append` and confirms
  `info` still discovers the bundle (ad-hoc legacy path still works,
  signature is intentionally invalid -- we do not run codesign verify
  here)
- runs an oversized-payload pack (32 MiB into a 16-MiB segment) and
  confirms it exits non-zero with both "FLAPI_RESERVED_BUNDLE_MIB"
  and "reserved|exceeds" in the error message

The Windows job uses pwsh; pack/info/unpack/sha256 logic mirrors
Linux via PowerShell idioms.

`create-release` now `needs` the four new jobs in addition to the
existing build + smoke matrix, so a broken pack on any platform
blocks the release.

The existing `integration-tests` job (which already runs on
ubuntu-24.04 amd64) auto-picks up `test_self_packaging.py` (#56) and
`test_env_overrides.py` (#57) via pytest discovery -- no change
needed there.

Note: on macOS we deliberately do NOT assert reproducibility because
the codesign step may include non-deterministic data; on Linux and
Windows the host-bytes + appended-archive layout is reproducible by
construction.

* fix: LocateBundle probes the macOS reserved segment too

Previously LocateBundle(path) only did the EOF tail scan. Only
LocateBundleInSelf() probed the __FLAPI/__bundle Mach-O section
first. That asymmetry meant flapi info / flapi unpack -- both of
which call LocateBundle directly against a given binary -- couldn't
find bundles that flapi pack wrote into the reserved segment on
macOS.

Caught by CI: pack-smoke-macos reported

  Packed 3 entries (683 bytes) into out-segment
  Bundle: none (filesystem mode)

The pack succeeded (3 entries went into the section), but info
checked only the EOF tail and saw nothing.

Fix: move the section-probe into LocateBundle(path), where every
caller benefits. LocateBundleInSelf becomes a thin wrapper. The
fall-through to EOF tail is preserved so --macos-append bundles
(legacy ad-hoc layout) remain discoverable.

Found by CI on PR #59 (#49 cross-platform smoke jobs).

* fix(pack): tolerate codesign failure on --macos-append + correct pwsh array-match

Two follow-up fixes surfaced by CI on PR #59:

1. macOS: `flapi pack --macos-append` always tried to codesign the
   output, which fails with "main executable failed strict
   validation" because the appended ZIP after __LINKEDIT puts the
   binary outside what codesign considers signable. That's the
   documented trade-off -- append-mode output is explicitly not
   notarisable. Now we warn and continue; only the
   reserved-segment path treats codesign failure as fatal.

2. Windows pack-smoke: `$info -notmatch "x"` on a PowerShell
   array returns the filtered subset, not a boolean -- so the
   negation in the if statement was always truthy. Pack and info
   both worked correctly (info actually listed flapi.yaml, sqls/...);
   the test logic was the bug. Join the captured lines into a scalar
   string before -notmatch.

Re-running the macOS leg should now show pack-smoke-macos passing
through the --macos-append step. Re-running the Windows leg should
get all the way to the reproducibility check.

Found by CI on PR #59 (#49 cross-platform smoke jobs).

* fix(pack): missing <iostream> for std::cerr warning on --macos-append

CI on macos-latest failed to build src/pack.cpp:
  error: no member named 'cerr' in namespace 'std'

Caused by the previous fix that warns instead of throws on the
--macos-append codesign-failure path. <ostream> doesn't drag
std::cerr in; we need <iostream> explicitly.

* ci: rerun (flake retry on integration-tests' sql-injection corpus)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self-packaging #7: 12-factor env-var hardening (FLAPI_CONFIG + FLAPI_LOG_LEVEL)

1 participant