Repository navigation
Fix issues: 82, 83, 84 - #118
Merged
believetimothy merged 2 commits intoOct 25, 2026
Merged
Conversation
|
@abercrombie04 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
github-actions Bot
pushed a commit
that referenced
this pull request
Oct 19, 2026
…125) This PR resolves the following assigned issues: - closes #79 - closes #80 - closes #81 ## Changes Made **#79 — docker: run soroforge serve and its migrations with Compose** (`docker-compose.yml`, `README.md`) - Adds two profiled services under the `app` profile, extending the existing Postgres service (untouched, healthcheck comment intact): - `migrate`: one-shot `soroforge migrate up`, starts after `postgres` is healthy, exits 0 on success. - `serve`: `soroforge serve`, waits on `migrate` via `depends_on: condition: service_completed_successfully` (the SoroVault pattern the issue names). - Secrets come from the environment only (`SOROFORGE_API_TOKEN` is required at the compose layer via `${...:?...}` so a missing token fails fast; the keystore is mounted read-only). Nothing secret is stored in the file. - Host port publish stays `127.0.0.1:8080:8080`, matching the binary's own loopback default (`config.DefaultAPIAddr`). - Plain `docker compose up -d` remains Postgres-only because both new services are profiled, so the current workflow is unchanged. **#80 — feat(cli): require typed confirmation before submitting to mainnet** (`cmd/soroforge/confirm.go`, `cmd/soroforge/commands.go`, `internal/config/config.go`, `soroforge.yaml`, tests) - `Network.Protected` (`protected: true` in `soroforge.yaml`); `mainnet` ships protected. - `deploy` and `upgrade` print what will happen (action, target, network, passphrase) and require the exact network name typed on stdin before anything is submitted. - Fails closed: `--yes` skips for CI (and says so on stderr); **without a terminal on stdin and without `--yes` the command refuses rather than hanging**; a read error or empty input is a refusal, not consent; `--dry-run` never prompts. Prompts go to stderr so `--json` on stdout stays parseable. - Tests (`cmd/soroforge/confirm_test.go`) cover prompt accepted, prompt rejected, `--yes`, non-TTY refusal, dry-run, and read-error refusal; config tests cover `protected` parsing. - Position on the HTTP API, as the issue asks: protected networks are **not** refused through the API in this PR — `/v1/deploy` and `/v1/upgrade` keep proceeding behind a write-scoped token, and the README says so. Making the API refuse protected networks (or require an explicit allow flag per token) is a product decision this PR deliberately does not pre-empt. **#81 — feat(store): record failed deploy and upgrade attempts** (`internal/store/*`, migrations, `internal/deploy/*`, CLI, tests) - New `Attempt` type (alias, network, action, stage `simulate|submit|confirm`, error text, tx hash when one was sent, time) and two `Store` interface methods, `RecordAttempt` / `Attempts`, implemented by **all three shipped backends** (MemoryStore, SQLite, Postgres). - Paired `000006_deployment_attempts` up/down migrations for both the `postgres/` and `sqlite/` dialect directories, following the repo's embed + golang-migrate convention; the reversibility tests' version gate moved 5 → 6, and the existing SQLite/Postgres dialect-parity test covers the new table. - `internal/deploy`: failures out of `runTransaction` now carry their submission-cycle stage in a transparent wrapper (`stagedError`; message text unchanged, `errors.Is/As` pass through `Unwrap`), and `Deploy`/`Upgrade` record every non-dry-run failure via a deferred guard so no error path can be missed. Recording failures can never mask the operator's original error. - Failures never enter `deployments`: `status`, drift detection, and `history` read confirmed-only rows (asserted in both the conformance suite and the deploy tests). - `soroforge history <alias> --failures` shows them (table and `--json`); README documents the flag and the semantics. - Key-material guarantee: `SanitizeKeyMaterial` redacts secret-seed-shaped text on every write path; tests assert a stored attempt error contains the redaction marker and never the seed, both at store level and end-to-end through a failing deploy. ## Verification (run on this branch head `22cd756`) - Baseline at `72d11e6`: `gofmt -l` empty, `go vet ./...` clean, `go build ./...` clean, `go test ./...` all 9 packages ok. - After: `gofmt -l` empty; `go vet ./...` clean; `go build ./...` clean; `go test ./... -count=1 -v`: **742 pass lines (tests incl. subtests), 0 fail**. - `go test -race ./internal/store/ ./internal/deploy/ ./cmd/soroforge/ ./internal/config/`: all ok. - `make check` (the testing command all three issues name): exit 0. - `make lint`: **not meaningfully run** — `golangci-lint` is not installed on this machine and the repo's lint target self-skips when it is absent. - `make test-integration` / the Postgres half of the store suite: **not run** — it needs `TEST_DATABASE_URL` against a live Postgres and this machine has no container runtime (docker/podman absent) and installing one was out of scope. The Postgres code path and the new `000006` pair are therefore only exercised via the conformance suite's Memory/SQLite runs and compile-level checks, same as CI-gated today. - #79 compose: validated statically and semantically, not by booting containers (no daemon here): YAML parses (`yaml.safe_load`); `migrate`/`serve` are profile-gated; `serve.depends_on.migrate = service_completed_successfully`; env var names cross-checked against `.env.example` and `internal/config/env.go`; `command: ["migrate","up"]` / `["serve"]` verified against the actual cobra subcommands; port default matches `config.DefaultAPIAddr`. `docker compose up --profile app` itself is the one criterion that needs a daemon and is stated as unverified. No new Go dependencies were added (stdlib only). `deployment_attempts` was added to the integration-test truncate list so the shared-DB suite stays clean. ## Overlap note Open PRs #118/#117/#110 touch some of the same files (`README.md`, `cmd/soroforge/commands.go`, `internal/deploy/service.go`, `internal/deploy/deploy.go`, `internal/config/*`) in different regions; none of them references #79/#80/#81. Merge conflicts there are theirs-to-resolve order-wise; nothing here edits their issue areas.
soroworks#120 landed the same changes first and later PRs built on them, so the conflicting files follow main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR resolves the following assigned issues:
Changes Made
#82 —
test(deploy): coverDefaultClientFactorywiringinternal/deploy/client_factory_test.go(new file, owned by this issue) with three test groups:TestDefaultClientFactoryWiresTheNetworkPassphrase— builds a client from aconfig.Networkfor testnet and mainnet (using the same passphrase literals as the existing deploy fixtures) and assertsNetworkPassphrase()returns exactly the passphrase configured for that network. This pins therpc_url→URLandpassphrase→NetworkPassphrasefield wiring, so a mix-up between the two fields (which would sign transactions for the wrong network) fails immediately.TestDefaultClientFactoryKeepsNetworksApart— builds clients for two networks back to back, the sequence a server serves when consecutive requests deploy to testnet and then mainnet, and asserts neither client picked up the other's passphrase.TestDefaultClientFactoryRejectsInvalidRPCURLs— table of five malformed endpoints (not a url,://missing-scheme,ftp://example.org, empty string,https://with no host), each asserting construction returns an error naming the offending value and no client.DefaultClientFactory(internal/deploy/service.go): parse withnet/url, require schemehttp/httpsand a non-empty host, otherwise returninvalid RPC URL %q: .... Rationale:stellar.NewRPCClientonly rejects an empty string, so any other malformed endpoint survived construction and failed later as an opaque error on the first request; checking in the factory names the bad value while the config file is still open. Construction makes no network call, so the tests need no server and nohttptest.deploy_testand only the public surface (deploy.DefaultClientFactory,config.Network), matching how other packages' external tests are written.#83 —
refactor(config): register constructor argument types in a tableinternal/config/args.go: replaced the singleswitchinArg.ToScValwith one ordered registry,argTypeRegistry []argTypeEntry{name, encode}, populated ininit():encodeBool,encodeU32, …,encodeVec,encodeMap,encodeXDR) registered in exactly one place.SupportedArgTypesis derived from the registry (built in the sameinit), never a hand-maintained literal — the drift the old design allowed (a type encoded but missing from the message, or listed but never encoded) is now impossible. The current order (bool, void, u32, i32, u64, i64, u128, i128, string, symbol, bytes, address, vec, map, xdr) and therefore every"supported: ..."error message are byte-identical to before.ToScValnormalises the type name with the samestrings.ToLower(strings.TrimSpace(...)), keeps the exactmissing type (...)andunsupported type %q (... use type: xdr ...)messages, and looks the name up in the registry.vecandmaprecurse throughArg.ToScValrather than calling encoders directly, so nested values take the same registry lookup and the samevec[%d]/map[%d].key/map[%d].valueerror wrapping as top-level ones.init()pattern is used because the pieces reference each other (registry holds the encoders,vec/maprecurse throughToScVal,ToScValreads the registry andSupportedArgTypes); written as initialization expressions that is a cycle the compiler rejects, andinitruns once after all package variables are set with deterministic order.Argdoc comment now describes the registry workflow instead of "extend the switch".internal/config/args_test.go: existing table-driven cases were moved to package-level vars verbatim (zero expectation changes — every pre-existing test passes unchanged, as the issue requires), plus:TestEveryRegisteredArgTypeHasATableCase— asserts every type inargTypeRegistryhas at least one case in the table test, so a new type cannot be registered without a test.TestArgToScValContainerTypes— extra nestedvec/mapcoverage exercising recursion through the registry.#84 —
feat(api): configurable CORS for browser dashboardsinternal/config/env.go:EnvAPICORSOrigins = "SOROFORGE_API_CORS_ORIGINS"andEnv.APICORSOrigins(trimmed inLoadEnv; included inRedactedsince origins are public).ParseCORSOrigins(raw)— splits the comma-separated value, trims, skips empty entries (a trailing comma is harmless), validates each origin, and errors on any invalid entry so a typo cannot silently disable or widen the policy. Empty input yields no origins, which is how CORS stays opt-in: unset ⇒ exactly today's behaviour of no CORS headers.ValidateCORSOrigin(origin)(exported soapi.NewRoutercan holdOptionsto the same rules) — rejects*anywhere with an explicit explanation:SOROFORGE_API_CORS_ORIGINS must not contain "*": list the exact origins the dashboard is served from. It also requires an exactscheme://host[:port]: no path, query, fragment, userinfo, or non-http(s) scheme. Rationale documented in code: the API can deploy contracts with the configured signing key, and browsers compare origins exactly, so accepting a path/trailing slash would configure a list that never matches with CORS silently off.internal/api/api.go:OptionsgainsCORSOrigins []string;NewRouterre-validates each origin (library callers get the same refusal at startup as the env var) and returns an error rather than a server.github.com/go-chi/cors(new direct dependency, v1.2.1) configured narrowly:AllowedOrigins= the exact list,AllowedMethods=GET, POST(only whatroutes()declares),AllowedHeaders=Authorization, Content-Type(the bearer token and JSON bodies),MaxAge= 10 minutes, and noAllowCredentials(the API authenticates with a bearer header, not cookies).OPTIONSrequests deliberately carry noAuthorizationheader, so the CORS handler answers them before the auth-wrapped routes are ever reached, while actual requests still pass throughauth.authorizein full.cmd/soroforge/commands.go: theservecommand parsesSOROFORGE_API_CORS_ORIGINSviaconfig.ParseCORSOriginsand passes the result toapi.Options, so a bad value (including*) fails at startup.README.md: documented the variable in the environment-variables table plus a "Browser dashboards (CORS)" section with an example, the opt-in rationale, the*refusal, and the preflight-without-token / actual-requests-still-authenticated behaviour.go.mod/go.sum: addedgithub.com/go-chi/cors v1.2.1.Tests
Access-Control-Allow-Origin; a disallowed origin gets no CORS header; preflightOPTIONSto a write route with no token returns 2xx with CORS headers (and the handler is never invoked); a tokenless actual request with an allowed origin still returns 401; empty config sends no CORS headers at all;*and malformed origins are refused by bothParseCORSOriginsandapi.NewRouter.make check— gofmt clean,go vet ./...clean,go test ./...all 8 packages OK (cmd/soroforge,internal/api,internal/catalog,internal/config,internal/deploy,internal/readiness,internal/stellar,internal/store), re-run with-count=1.make lintreports golangci-lint not installed and exits 0.