Skip to content

Releases: angvp/tango

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 19:06

Added

  • Sequenced realtime frames. A Logic that also implements realtime.Framer frames every payload admitted to each member of its room with a per-connection sequence: 1 for the join snapshot, then 2, 3 and so on, restarting at 1 on reconnect. The host owns the wire format. Join fails with realtime.ErrEmptyFrame (or the wrapped Frame error) when the framed snapshot is empty or fails, and the failure does not close the existing connection or admit the new peer. Admitted sends from Snapshot keep their sequence and are not rolled back; a send whose Frame errors or overflows the queue closes the existing connection, which stays closed. A Logic that does not implement Framer is unchanged. See the realtime guide.

  • Google Cloud Storage adapter (experimental, separate module). storage/gcs is a storage.Store on Google Cloud Storage, in its own Go module so no cloud SDK reaches a project that does not import it. gcs.New authenticates with application-default credentials, workload identity or explicit credentials, and gcs.NewFromClient takes a client the host built. Writes are create-only and atomic, and a retried write that already committed is confirmed by a per-write token, checksum and size rather than reported as a collision. Experimental: tested against an in-process emulator, not yet against a real bucket. The module is versioned and tagged separately as storage/gcs/vA.B.C, after the root v0.7.0 tag it requires. See the uploads guide.

  • Admin bulk delete. admin.Options.BulkDelete lets a staff account delete the checked rows of one changelist page after a confirmation dialog. The delete is one all-or-nothing transaction that cascades like a single-row delete, an invalid selection (empty, over a page, a wrong count, a repeated or non-canonical key) is rejected whole, and each committed delete logs one admin.EventBulkDelete (tango.admin.bulk_delete) event with model, selected_count (the rows selected, not the database rows a cascade deleted) and username. It is off by default, needs JavaScript and never spans more than a page. See admin registration.

  • Grouped filters. Query.Filter is an optional boolean tree ANDed with Where and Any, built with db.Cond, db.And, db.Or and db.Not (the Expr and ExprOp types). A Query without a Filter produces exactly the SQL it did before. A filter holds at most MaxFilterNodes (100) nodes and MaxFilterDepth (8) levels.

  • IN, IS NULL and IS NOT NULL. OpIn with db.In(field, slice) (a nil or empty list matches nothing on every dialect; at most MaxInValues, 1,000, values per list and MaxQueryArgs, 10,000, bind values per query; a zero time.Time is rejected), and OpIsNull/OpIsNotNull with db.IsNull/db.IsNotNull, which take no value. All three work in Where, Any and Filter. A foreign key left at zero is stored as NULL, so OwnerID = 0 still never matches (unchanged); use IsNull for unset keys.

  • Store.First and Store.Exists. First reads one row (primary-key order unless OrderBy is set, ErrNotFound naming the model when none, the destination untouched on any error); Exists reports whether any row matches. They validate and build SQL exactly as List and Count do.

  • Typed query errors. ErrInvalidQuery, ErrQueryLimit, *QueryError and *QueryLimitError. Every malformed query is refused before any SQL runs. The existing Where/Any validation errors keep their text and now also match ErrInvalidQuery.

  • Locale resolvers. i18n.Resolver (a locale source that also names the request headers it depends on), the helpers FromQuery, FromCookie and FromHost, Chain (the host's order, then Accept-Language), FuncResolver for a plain function, and MiddlewareResolver to install one. Each helper returns only a locale with a registered catalog and none sets a cookie. Middleware and DefaultLocaleResolver are unchanged.

  • i18n/i18ntest.Isolate. Test support: i18ntest.Isolate(t) gives a test an empty i18n state (catalogs, the OnMissing hook and its once-state, the observed keys) and restores the previous state in t.Cleanup. A test that uses it must not be parallel.

  • i18n.Keys and i18n.Validate. T records each (key, fallback) pair the first time it runs with a resolved locale (nothing is recorded, and nothing allocated, when no locale is set). Keys() lists them; Validate() checks every catalog against them and returns a Report with Problems (ProblemMismatch, ProblemEmpty, ProblemConflictingFallback when a key is seen with two fallbacks, ProblemRegistryFull) and the advisory, sorted Unobserved catalog keys. It covers only what has run in this process, and the registry holds at most 4,096 keys, so fallbacks must be stable literals.

  • i18n.OnMissing. A development hook that reports a missing translation (an absent key, an empty value or a value whose format verbs do not match the fallback's) with the resolved locale and key, at most once per pair. It is nil by default, runs outside the package's locks, and OnMissing(nil) clears it.

  • The admin's remaining English is translatable. <html lang> follows the selected catalog on every admin page and on the Markdown preview document (en when none is selected). The Markdown and JSON widgets' tab labels, buttons, character count, aria-labels, iframe title, script messages (Nothing to format., Preview failed. and the rest, delivered as data-msg-* attributes only when a locale was resolved), and the JSON validation errors now use admin.* keys with the current English as fallback. An app without i18n renders the same output as before. A test keeps new literal text out of admin's templates.

  • Translatable accounts pages. The register, login, password-reset and email-verification pages and their form errors (including the generic login error, which stays one message for every failure) go through i18n.T under accounts.* keys with the current English as fallback. A page declares <html lang> only when a catalog was selected, so an app without i18n renders exactly what it did before. The JSON API's error text stays English; clients branch on code.

  • Accounts emails in the request's language. The verification and reset emails are rendered in the locale of the request that queued them (accounts.email.* keys, English fallback). The locale is captured when the email is queued, before any account lookup; the queued job keeps nothing else of the request. A translated body that lacks the %s link placeholder falls back to the English body, so the link is never dropped.

  • The tango newproject scaffold points at the i18n guide. The generated middleware list carries a two-line comment saying how to opt in (i18n.Middleware(nil) and catalogs). The generated project is otherwise unchanged.

  • examples/api-with-admin in Spanish. The example serves the admin in Spanish end to end: a complete es catalog, a small es-MX override, a ?lang= → cookie → Accept-Language order and a language switcher, with a test that walks the pages in both locales and checks OnMissing and Validate. tanGO itself still ships no catalogs.

  • examples/accounts-mail in Spanish. Registering, confirming an address and resetting a password run in Spanish, emails and links included, with a test that walks the flow and fails on any missing key.

Changed

  • The i18n middleware sets Vary and Content-Language. With i18n.Middleware(nil) every response gets Vary: Accept-Language, so a cache in front of the app no longer serves one language to another visitor; MiddlewareResolver(Chain(...)) sends the tokens its resolvers need (Cookie for FromCookie). Content-Language names the selected catalog's locale, and only when a catalog was selected. Existing Vary values, including ones a downstream handler adds, are merged without repeating a token (ignoring case). A resolver passed as a plain function adds no Vary (use FuncResolver to declare one). Nothing changes for an app without the middleware.

  • A bad catalog value falls back instead of printing garbage. i18n.T now ignores a catalog value that is empty or whose format verbs differ from the fallback's (positional verbs such as %[2]s count, so reordering arguments is still fine), tries the base language, and then shows the fallback. Before, "Nuevo" for "New %s" rendered Nuevo%!(EXTRA string=Pet) and an empty value rendered nothing. A literal % in a fallback must be %% once arguments are passed.

  • Upgrade note: literal % in i18n fallbacks. The fallback is a format string whenever arguments are passed, so a bare % is read as a format verb and must be escaped. Change i18n.T(ctx, "sale.label", "Save 10% on %s", item) to i18n.T(ctx, "sale.label", "Save 10%% on %s", item). Search the project's i18n.T calls and catalogs for % followed by a letter or space before relying on the new fallback rule.

Fixed

  • Locale matching ignores case and the -/_ separator. Accept-Language: es-mx, ES-MX and es_MX now select a catalog registered as es-MX (before, es-mx skipped the regional catalog and ES-MX matched nothing, not even es). The locale the middleware stores is the catalog's own spelling, and catalogs registered under two spellings of one locale are merged.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 16:10

Added

  • Admin operational dashboard (opt-in). admin.WithDashboard(admin.DashboardConfig{Status, Activity}) turns /admin/ into a page with database and migration status and recent request and job activity; without it, or with neither source set, the landing page is unchanged. The status comes from a function you supply (build it from tango.Status), and the activity from any observability.Snapshotter. A failing or panicking source marks only its own card "unavailable" and logs the cause; the error text never reaches the page. New in observability: Snapshotter and Snapshot (a read side that is separate from Recorder), NewWindow (an opt-in in-memory collector: the last 60 minutes in one-minute buckets, HTTP requests by status class and latency, job runs by outcome, nothing per route or job, reset on restart, labelled "this instance only"), and Multi (feed several recorders at once). It is a starting point for "is this instance healthy right now", not a metrics store; keep an external monitoring system for history, alerts and several instances.

  • MySQL 8.4 support. mysql://user:pass@host:3306/db in TANGO_DB_DSN selects the new db.MySQL dialect, with github.com/go-sql-driver/mysql as the driver. Migrations, Store, the admin, tango newproject --dialect=mysql and testdb (a fresh database per test; TANGO_TEST_DSN=mysql://…, testdb.MySQLOnly, testdb.NotMySQL) all work on it, and CI runs the suite on MySQL 8.4. Tables are InnoDB with the utf8mb4_bin collation; timestamps are DATETIME(6) in UTC; connections use READ COMMITTED unless the DSN sets another isolation level, and the default pool recycles a connection after five minutes. MariaDB is not supported. Differences to know: an unbounded string that is unique or indexed is VARCHAR(255) (a longer value returns a *db.ValueTooLongError), LIKE is case-sensitive, and a migration that fails part-way cannot roll back (the error names every applied step and writes no tracking row). See the setup guide. A project with a shell/main.go from before this release must copy the new databaseLabel from the shell guide before using MySQL, or tango shell prints the DSN's password.

  • Admin dark mode. Every admin page now follows the browser's light or dark preference, and signed-in staff can pick Light, Dark or System from the sidebar footer; the choice is kept in a tango_admin_theme cookie scoped to /admin/. The light theme is nearly unchanged: input, checkbox and secondary-button borders are a shade darker, and primary and danger buttons darken on hover, so they meet WCAG contrast. Colours are now semantic tokens (bg-surface, text-ink, border-line, the status family, and so on) generated from one palette table, with contrast tests over its pairs in both themes. Widgets that hard-code light colours stay light, so switch them to the token classes in the admin guide; the Markdown preview stays a white canvas in both themes. There is nothing to configure.

Changed

  • AccessLogger is free when its logger discards Info events. It used to wrap the response and build the event's attributes for every request and only then let the logger drop it (3 allocations a request). It now asks the logger first and calls the next handler directly. A logger that logs Info behaves as before; the level is read on every request, so a slog.LevelVar still takes effect at once.

  • Dependencies. go.mod now requires chi 5.5.0 (was 5.3.2), golang.org/x/crypto 0.58.0, x/sys 0.49.0, x/term 0.47.0 and modernc.org/sqlite 1.60.1, and adds github.com/go-sql-driver/mysql 1.10.1 for the MySQL dialect. A project that depends on tanGO is raised to at least these versions the next time it updates; run go mod tidy.

Documentation

  • Choosing MaxOpen. The configuration guide now explains how the size of the database pool shapes tail latency, with measured figures, and suggests starting near the database's CPU count.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 01:30

Added

  • OpenAPI documents. openapi.New(openapi.Options{...}) in InstalledApps serves an OpenAPI 3.1 document of every included route at /openapi.json (opt-in: nothing is served otherwise). openapi.Dev is the same app served only when TANGO_OPENAPI=1, so a deployed process exposes nothing unless it sets the variable. Each route is listed with its method, path, path parameters, name and a tag from its prefix. tango.Doc(tango.RouteDoc{...}) adds a summary, a request body, query parameters, responses and a bearer requirement, with schemas reflected from Go types (encoding/json tags, nested and recursive types, varchar=n as maxLength); a type can name its schema with SchemaName(). tango check fails and names a route whose documented types cannot be described. The accounts JSON endpoints document themselves; the admin's pages and the cookie-flow pages are hidden from the document. RouteRegistry.Described and tango.DescribedRoute expose the route table to tools. tango newproject now leaves a comment in project/project.go saying where to add it. See OpenAPI documentation.
  • tango openapi [-o file] prints or writes the document (the new -tango-openapi app-side flag, handled by DispatchFlags; DumpOpenAPI, OpenAPIProvider and ErrNoOpenAPI). The output is the same bytes on every run, and a failed run leaves an existing file untouched.
  • Swagger UI. openapi.Options{UI: swaggerui.New()} adds a browsable page at /docs/ from Swagger UI 5.17.14 embedded in the binary (Apache-2.0; no network request). It is the separate package openapi/swaggerui and adds about 1.7 MB to a binary that imports it.

Changed

  • Database connections are reused under load. database/sql keeps only 2 idle connections, so a server handling more than 2 requests at once closed and reopened a database connection on almost every request (measured: about 10,900 PostgreSQL sessions opened in one 15-second run of single-pet reads). ServeContext now raises the idle limit to 32 unless your app bounded the pool with SetMaxOpenConns. tango.WithDBPool(tango.DBPool{...}) sets the limits explicitly; tango.WithDBPool(tango.DBPool{}) restores the previous behavior. The number of open connections is unchanged (unlimited).

  • Foreign keys are not indexed unless you say so (guidance). A fk= column is not indexed by itself, so filtering or joining on it scans the table (measured: list on a 10,000-row table ran at about 1,300 requests a second without the index and about 3,200 with it). Write tango:"fk=Category,index" on foreign keys you filter or join on. Indexing foreign keys by default, as Django does, is planned for a later minor release; this entry is the notice.

Fixed

  • The admin no longer comes up silently empty. The admin builds its pages when it is installed, so a model registered by an app listed after admin.New never appeared in it. tango check (and -check) now fails and names those models, the tango newproject scaffold says to add apps above admin.New, and tango newapp repeats it when the project installs the admin. Move admin.New after your apps.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 22:41

Added

  • JSON fields. tango:"json" on a string field declares it holds JSON text (model.FieldMeta.JSON). Store.Create and Store.Update refuse text that is not valid JSON with a *db.InvalidJSONError that matches db.ErrInvalidJSON and never contains the submitted text; the empty string is unset, null is valid, and the stored text is exactly what was given. It is ordinary TEXT on SQLite and PostgreSQL, so adding the tag to an existing field generates no migration and does not scan existing rows; reads are not checked. json cannot be combined with varchar=n, text, pk, fk, unique or index. See ADR 0059.

  • JSON fields in the admin. A tango:"json" field gets admin.JSON() by default (Edit and Formatted tabs) with no Options.Widgets entry; an explicit entry still wins, and read-only and changelist rendering are unchanged. A save the store refuses for invalid JSON, including through a non-JSON widget such as admin.Textarea(), re-renders with a 422 and a translatable field error (admin.error.invalid_json, "Not valid JSON.") beside the field, keeping the text as typed. See admin registration.

  • Admin refinement. admin.JSON() edits a string field as JSON (syntax checked on save, the submitted text stored exactly, a browser-side Formatted tab). admin.Markdown(...) edits Markdown in a monospace textarea with a character count and, when you pass admin.WithMarkdownPreview, a sandboxed Preview tab from your own renderer: tanGO ships no parser or sanitizer, and you decide how HTML is sanitized (ADR 0058). Options.Filters adds boolean and foreign-key filters to the changelist (they combine with search and ordering; an over-large foreign-key filter still applies a primary key given in the URL). Options.Display with admin.DisplayFunc and admin.Display renders computed changelist cells, such as a file's size and an authorized download link, without admin knowing about storage; admin.ByteSize and admin.ShortHash help. admin.FieldError lets a widget report an error beside its own field. Forms now render HelpText and mark invalid fields with aria-invalid and aria-describedby; the changelist gets a labelled search box and filters, a caption and column scopes. Options.Labels now also names a field's changelist column when set. See admin registration and ADRs 0057 and 0058.

  • Bounded strings. tango:"varchar=n" declares a string field of at most n characters (1 to 10,485,760), and tango:"text" states an unbounded one explicitly; a bare string stays unbounded text and generates no migration. model.FieldMeta.MaxLength carries the limit and a malformed tag fails Register with model.ErrInvalidFieldTag. Store.Create and Store.Update refuse a longer value before any SQL with a *db.ValueTooLongError (db.ErrValueTooLong), counting runes the way PostgreSQL counts characters; SQLite does not enforce the length and raw SQL is not checked. tango makemigrations writes a VARCHAR(n) column (Column.Length, additive, with no CHECK), generates the widening changes (varchar(n) to text or a larger varchar) and the narrowing ones (text to varchar(n) or a smaller varchar), and a narrowing checks the existing data in Go first, failing with the number of rows and one offending key instead of truncating anything. A migration of narrowings is reversible; a widening makes its migration irreversible. The admin renders maxlength and shows a translatable per-field message (admin.error.too_long) for a refused save, and admin.FieldContext gains MaxLength. Built-in models are not retagged. See models and tags and changing a field's type.

  • Accounts for a single-page app. accounts.WithJSON mounts six bearer-only JSON endpoints under /accounts/api/ (register, login, password reset request and confirm, verify, resend verification) for a separate client, sharing the HTML flow's rules, limiters and outbox. The host supplies the token side through accounts.JSONAuth (Issue, Authenticate) and AuthGrant, so accounts imports no token package; JSON mode never reads or sets a cookie and has no logout. JSONConfig also takes the emailed link templates VerifyURL and ResetURL ({token} once; prefer a fragment), an OnRegister hook that runs inside the account's transaction with a Registration (a FieldError or *db.ValueTooLongError answers 422), and ResolveIdentifier so login can take a username. Requests are strict (415, 413, 400 invalid_body), errors are {"error", "code", "fields"?}, and every response is no-store. New db.Store.InTx runs a callback in one transaction (commit, rollback on error, rollback and re-panic on panic; nested calls join). See the accounts guide, the new examples/spa-accounts and ADRs 0050 and 0051.

  • Accounts answers invalid JSON as a field error. In the JSON accounts API, a *db.InvalidJSONError from a registration hook's tx.Create (alone or joined with errors.Join) answers 422 invalid_field, keyed by the Go field name with the message must be valid JSON, instead of a generic 500; the document is never echoed. See the accounts guide.

  • File uploads and storage. The new storage package is an explicit object store for uploaded files, used from ordinary Views: Put streams a file in, always sniffs its content type, enforces a required size cap and an optional allowed-types list, and returns a generated opaque key with the size and SHA-256; Stat, Open (a byte range) and Delete complete the Store. There is no FileField, no file model, no listing and no signed URLs: the host keeps the key and metadata in its own model and authorizes every access. storage.Upload reads one multipart file with bounded memory, and storage.Serve delivers an authorized object with ranges, conditional requests, attachment by default and nosniff. Backends: storage/local (atomic, os.Root-contained, for development and one host), storage/s3 (Amazon S3, Cloudflare R2 and other S3-compatible providers, in its own Go module so no cloud SDK reaches your go.mod), and storagetest with an in-memory Store and the conformance suite every Store passes. See the uploads guide, the new examples/uploads and ADRs 0052 to 0054. Native Google Cloud Storage is a later release.

  • Caching. The new cache package is a small, explicit cache for recomputable reads: a bytes-only cache.Store (Get, Set with a TTL of up to 30 days, Delete) where a miss is not an error, one portable key rule, cache.Prefix for namespaces and bulk invalidation by version, typed JSON helpers (GetJSON, SetJSON, ErrCorrupt), and the fail-open FetchJSON read-through that reports cache failures to an OnError hook and never fails the request because the cache is down. cache/local is a bounded in-process store (entry limit, optional byte budget, least-recently-used eviction); cache/redis (Redis and Valkey) and cache/memcache are their own Go modules, so no client library reaches your go.mod. cachetest.Run is the conformance suite every backend passes. There is no response, fragment or ORM caching, no automatic invalidation, and nothing logs. See the caching guide, the new examples/cache and ADRs 0055 and 0056.

Changed

  • An admin changelist ?page= beyond the last page now shows the last page, so the rows and the pager always describe the same page (before, it showed an empty list under a pager that named the last page).

  • The admin foreign key select now starts with a "— None —" choice (translation key admin.fk.none), and an unset foreign key shows an em dash in the changelist instead of 0. Saving "None" stores the same unset value (NULL) as before.

  • POST /admin/logout/ now requires the session's CSRF token when the request carries a session cookie, so a cross-site form cannot sign an admin out. A missing or wrong token gets 403 and leaves the session in place. A request without a session cookie still redirects to the login page.

  • The admin sidebar footer no longer says "Signed in over Basic Auth" (admin has used session logins since 0.3). It now shows "Signed in as " with a Log out button that posts to /admin/logout/ with the session's CSRF token. The translation key admin.layout.signed_in is replaced by admin.layout.signed_in_as and admin.layout.logout.

  • /accounts/login/ now always spends one password comparison, even for an unknown or inactive account, so its timing no longer says whether an account exists. Nothing else about the HTML flow changes.

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 09 Oct 17:59

Fixed

  • db.Query with an Offset and no Limit failed on SQLite with a SQL syntax error (SQLite refuses OFFSET without LIMIT). List now skips the rows and returns the rest on both dialects. It also made tango shell's List(model, map[string]any{"offset": n}) fail the same way.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 14:27

Added

  • tango shell: a Go prompt on your project's models and database, with nothing served. It is a Yaegi interpreter (pinned at v0.16.1) run by the project's own shell/main.go through the new shell package, so the server never links it. Helpers Models, Describe, Get, List, Count, Create, Update and Delete work on any registered model addressed as app.Model, with plain-map queries and rows; -c EXPR and piped input make it scriptable; --readonly refuses writes; Options.Helpers adds your own functions as project.Name. At a terminal it has line editing, per-project history under your cache directory (TANGO_SHELL_HISTORY=off disables it), and Ctrl-C that clears the line, or ends a running evaluation with status 130. It says what Go the interpreter cannot run (min, max, clear, some generics, ranging over a function or an integer) and keeps the session through errors and panics. It runs your code with your project's credentials; it is not a sandbox. See the shell guide, ADR 0048 and ADR 0005. Adds github.com/traefik/yaegi to the root module; only a binary that imports shell links it.
  • admin.WithTrustedProxies and accounts.WithTrustedProxies: behind a reverse proxy, the failed-attempt limiters count the client address your proxy saw (the rightmost X-Forwarded-For entry outside the networks you name) instead of the proxy's own, so users no longer share one bucket. Opt-in: without it nothing is read from X-Forwarded-For and behaviour is unchanged, and only requests from the named networks may supply a forwarded address. The limiter also no longer keeps an entry for an address that has no recent failure. See behind a reverse proxy.
  • Running more than one instance: what is shared across instances (sessions, accounts, tokens), what is per process (the login limiter, ratelimit, realtime rooms, the accounts mail outbox and cooldown) and the workarounds, with ADR 0047 recording that shared state is not built yet and what would reopen it. No API changes.
  • A tango tui guide, with screens checked against the code by a test, and the promise about the dashboard in versioning and compatibility: the command, its terminal requirement, confirmation before database changes and delegation to the supported operations are covered; layout, wording and key bindings are not.

Changed

  • tango newproject writes two more directories. project/project.go holds func Config(store *db.Store) tango.Config: the installed apps, middleware and ordinary configuration that main.go used to build inline, so the server and the shell register the same apps from one list. main.go opens the database, builds the store and calls project.Config(store). shell/main.go is the program tango shell runs. tango newapp now says to install a new app in project/project.go when that file exists. Existing projects keep working as servers; to get a shell they follow the documented upgrade, a diff tested against the scaffold as it was before (ADR 0048).
  • The tutorial and the examples/board project list their apps in project/project.go; the board's token service and live feed are built inside project.Config, which stops the process with their error message when they cannot start.
  • tango admin create and tango admin resetpassword use TANGO_ADMIN_PASSWORD when it is set and non-empty, with no prompt. It was documented but never read. The value is never printed; the command names the variable instead.
  • tango migrate says what it did: applied 2 migrations: admin/0001_auto_…, blog/0002_auto_…, or no pending migrations, instead of always printing migrations applied. A project gets the new wording when it is built against this version; the rollback line is unchanged.
  • tango newproject ends with the real first-run order (tango makemigrations, tango migrate, tango admin create <username>), and the generated project prints admin: http://localhost:8000/admin/ after listening on (not with --no-admin). Existing projects are unchanged.
  • tango tui:
    • the greyed-out "Shell" item is gone;
    • "Apply pending migrations" and "Roll back the latest migration" grey out with a reason when status shows they cannot run (nothing to apply, nothing to roll back, or project status incomplete);
    • the confirmation prompts say what they will do.

Security

  • ratelimit.RemoteIPKey(trustedProxies...) could be made to give a client the key it chose. It took the leftmost X-Forwarded-For entry, which the client writes when the proxy appends to the header (nginx's default), and it did not check that the value was an IP address, so a client behind a trusted proxy could rotate its key to evade a limit or flood the limiter with new keys. It also fell back to X-Real-IP. It now reads X-Forwarded-For only from a trusted peer, parses every entry as an IP address, walks the list from the right skipping trusted proxies, and takes the first other address, which is the one your nearest trusted proxy saw. X-Real-IP is ignored, and a valid address is returned in canonical form. Unusable data falls back to the peer's address. The failed-login limiters of admin and accounts (admin.WithTrustedProxies, accounts.WithTrustedProxies) use the same resolver. This changes behaviour under the compatibility promise's security exception.
    • Upgrade: nothing changes without trusted proxies. With them, check that your proxy adds the address it sees to X-Forwarded-For (appending and replacing both work; passing the header through untouched does not). If you relied on X-Real-IP, make the proxy put the client address in X-Forwarded-For. Keys for IPv6 clients are now lower-case and compressed, and IPv4-mapped IPv6 addresses are keyed as IPv4, so a limit that was counted under the old spelling restarts once.

Fixed

  • tango admin create and resetpassword no longer show the password as you type it on a terminal. Piped input is read as before. This adds golang.org/x/term to the root module (ADR 0046).
  • Ctrl-C leaves tango tui even while a confirmation is showing; it used to be ignored until you answered.
  • Projects created by tango newproject now shut down gracefully: the generated main.go calls tango.ServeContext under signal.NotifyContext for Ctrl-C and SIGTERM, so in-flight requests finish and registered Lifecycle components stop. It called tango.Serve, which never reacts to a signal. Existing applications are unchanged; swap in ServeContext as the application lifecycle guide shows.
  • tango tui no longer ends the session when an apply or rollback fails. It shows Last action failed: … with the exit code and the command's last error line, refreshes the status, and returns to the menu; if the refresh also fails, both errors show.
  • Stopping the server started from tango tui with Ctrl-C or SIGTERM is a clean stop: tango waits for the server, prints server stopped and exits 0. It used to die with the signal. A server that fails, or exits with no signal, keeps its exit code.
  • tango tui explains a status failure (run tango check; an older main.go may need tango.DispatchFlags) before the raw error, which is kept.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 00:14

Changed

  • Projects created by tango newproject opt in to everything new in this release:

    • LoadConfigFromEnv(tango.WithPortFromEnv()), which replaces the hardcoded :8000 and honours TANGO_ADDR and PORT;
    • RequestID, Recoverer and AccessLogger;
    • a global MaxBodySize(1 << 20);
    • MiddlewareScopeAll.

    Existing applications keep their behaviour unless they opt in.

  • accounts models gain Account.EmailVerifiedAt and a new AccountToken model, for password reset and email verification. Every app that installs accounts must run tango makemigrations and apply the result with -migrate, whether or not it enables mail: the change only adds a column and a table, so it needs no --rename or --allow-drop. Existing accounts read as unverified; nothing is gated on it unless an app uses accounts.RequireVerified.

  • The route attribute is now the route's registered pattern in every log and metric for a request, trailing slash included (/items/{id}/), under both middleware scopes. The tango.http.access, tango.http.panic and tango.http.request_id_generation_failed logs used to drop a trailing slash (/items/{id}), while ctx.Logger(), tango.http.view_error and the HTTP metrics did not, so the same request reported two routes. Dashboards or alerts that match these three events on a trimmed route need the registered spelling. The attribute key is unchanged.

Added

  • A mail package for outgoing email: mail.Sender, Message with in-memory attachments (sent as multipart/mixed, 10 MiB of raw attachments by default), and three senders. NewSMTPSender/SMTPSenderFromEnv (TANGO_SMTP_URL) always encrypt: smtp:// requires STARTTLS, smtps:// is TLS from the start, and smtp+insecure:// is plaintext to a loopback host only, with an explicit port. Errors never contain credentials, recipients or messages. WriterSender is for development, and mail/mailtest is for tests. See ADR 0044 and the mail guide.
  • accounts.WithMail turns on password reset (/accounts/password-reset/) and email verification (/accounts/verify/), and accounts.RequireVerified guards a View on a verified email. Reset never reveals whether an address has an account, links are single-use, hashed, tied to the address they were sent to and built on a configured BaseURL, and GET never uses a link. Emails go through a bounded in-memory outbox, which logs tango.accounts.mail_failed and tango.accounts.mail_dropped. Without WithMail, accounts behaves as before. See ADR 0045.
  • Config.NotFound and Config.MethodNotAllowed Views replace the router's 404 and 405 responses (tanGO sets Allow for a 405). The Allow header lists every method routed at that path, including a non-standard one the app registered. Under MiddlewareScopeAll they run inside global middleware and are observed; under MiddlewareScopeRoutes they run outside it.
  • Config.MiddlewareScope: with tango.MiddlewareScopeAll, global middleware wraps the whole router, so Unmatched requests (the router's 404 and 405) are logged, counted, recovered and body-limited, reported with route "(unmatched)". Global middleware then runs before routing, so it can't read route parameters or rewrite which route answers. The default stays MiddlewareScopeRoutes, today's behaviour, and may become MiddlewareScopeAll only after a minor release of notice; set the scope explicitly to keep yours. See ADR 0043.
  • tango.LoadConfigFromEnv(tango.WithPortFromEnv()) falls back to the PORT variable hosting platforms set: the address is TANGO_ADDR, else ":"+PORT, else :8000. Without the option, LoadConfigFromEnv is unchanged.
  • tango.MaxBodySize(n) middleware caps request bodies: an oversized body gets 413 with {"error":"request body too large"}. When several limits apply, the most restrictive wins.

Security

  • ServeContext (and Serve) now drop a client that takes more than 10 seconds to send a request's headers, so a slow-header (Slowloris-style) client can't hold connections open indefinitely. This changes default behaviour under the compatibility promise's security exception. tango.WithReadHeaderTimeout(d) sets another bound, and WithReadHeaderTimeout(0) restores the old, unbounded behaviour.

Fixed

  • tango newapp now prints how to install the new app: the import path read from go.mod and the InstalledApps entry to add to main.go. A new app used to be created silently and answer 404 until wired in.
  • The compatibility page promised a JSON body for every 403, 405 and 429 from /accounts/*. It now promises only the JSON accounts sends, for a rejected CSRF token (403) and rate limiting (429, with Retry-After). Closed registration's 403 is an HTML page, and the router's 405 has no body. A test pins that contract.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 15:09

Changed

  • Breaking: the database is chosen by TANGO_DB_DSN's scheme alone, and TANGO_DB_DIALECT is retired: setting it is now an error. tango.LoadDBDSNFromEnv and tango.LoadDBDialectFromEnv are replaced by tango.LoadDBConfigFromEnv, which returns a db.DSN carrying the dialect, driver name and connection string together.
    • Upgrade: put the scheme in the DSN (TANGO_DB_DSN=postgres://…, or sqlite://app.db; unset still means sqlite://app.db), drop TANGO_DB_DIALECT, and replace the two loader calls with dsn, err := tango.LoadDBConfigFromEnv() followed by sql.Open(dsn.Driver, dsn.Source) and dsn.Dialect. Projects generated by tango newproject from this release on already do this.
  • Breaking: tango makemigrations refuses to write a migration that drops a table or column unless each drop is authorised with --allow-drop app.Model or --allow-drop app.Model.Field, and refuses model changes no migration can express (a narrowing type change, a change of primary key or of a foreign key's target). A refused run writes nothing and prints the commands to choose between.
    • Upgrade: if a run is refused, declare a rename with --rename or authorise the drop with --allow-drop, as the error message shows.
  • Breaking: migration.Diff and migration.DiffModels return an error alongside the migrations. These are part of the CLI-internal migration surface, which applications don't normally call.
    • Upgrade: handle the second return value.
  • SQLite writers wait up to five seconds for a held lock instead of failing at once with database is locked.
  • Breaking: realtime/websocket.View gained a variadic ...ViewOption parameter. Calls compile unchanged, but code that stores View in a variable of its old function type no longer compiles.
    • Upgrade: add ...websocket.ViewOption to that variable's function type, or call View directly.

Added

  • db.ParseDSN, which parses a scheme-qualified DSN into a db.DSN.
  • Renaming a field or a model with tango makemigrations --rename, which keeps the column or table and its rows.
  • Widening type changes (integer to real or text, real to text, boolean to integer or text) through the new AlterColumnType step.
  • The public testdb package: an app's own tests get a fresh database for the run's Test dialect, chosen by TANGO_TEST_DSN.
  • examples/board, the tutorial's app, tested end to end on SQLite and PostgreSQL.
  • realtime/websocket.View takes options: WithOriginPatterns and WithInsecureSkipVerify configure the upgrade's origin check.

Fixed

  • Dropping a column on SQLite keeps everything else about the table: rows, indexes, unique constraints, foreign keys in both directions, defaults, NOT NULL and the primary key.
  • NULL reads as the zero value, and an unset foreign key is written as NULL.
  • Cookies are marked Secure behind a TLS-terminating proxy that sets X-Forwarded-Proto.
  • Application sessions work on PostgreSQL.
  • Times read back in UTC on every dialect.
  • Generated migrations order their steps, and migrations across apps, by foreign key, so they apply on PostgreSQL.
  • Migration files generated by v0.0.1 and v0.0.2 apply and roll back on PostgreSQL even when a table references one whose name sorts after it: the runner now creates referenced tables first and drops referencing tables first within a migration.
  • Cascade delete handles circular foreign key references.
  • Every SQL identifier tanGO generates is quoted, so a model named after a reserved word (User, Order) works on both dialects.
  • Creating a row with an explicit ID on PostgreSQL advances the ID sequence.
  • tango newproject keeps its standard-library imports in one sorted group.

v0.0.2

Choose a tag to compare

@angvp angvp released this 28 Sep 02:46

Overview

v0.0.2 introduces comprehensive observability, graceful shutdown, rate limiting, and real-time WebSocket enhancements to the Tango framework.

Features

Observability & Logging

  • Structured HTTP Logging: Added request IDs and access/metrics middleware for comprehensive request tracking
  • Observability Package: New observability package with a Recorder for centralized metrics and structured logging
  • Realtime Instrumentation: Hub operations now instrumented with the shared Recorder for visibility into WebSocket connections

Application Lifecycle & Graceful Shutdown

ServeContext: New ServeContext for lifecycle-aware graceful shutdown
Lifecycle Management: Lifecycle registration and stop-phase budget sharing across all Lifecycle.Stop calls
Realtime Integration: Realtime chat Hub now properly integrated into graceful shutdown process
Documentation: Comprehensive docs on application lifecycle and graceful shutdown patterns

Rate Limiting

Token Bucket Implementation: New ratelimit package with token-bucket core algorithm
Flexible Key Functions: KeyFunc and RemoteIPKey for flexible rate limit key extraction
Middleware Integration: Rate limit middleware ready to use in any HTTP handler
Safety Guarantees: Handles impossible costs, nil CIDRs, and overflow-safe bucket cleanup with opportunistic cleanup

WebSocket Enhancements

Configurable CORS: WebSocket View now allows configuration of upgrade's origin check for better CORS control

Bug Fixes

  • Fixed handling of nil trusted CIDRs in RemoteIPKey
  • Fixed impossible cost rejection in ratelimit.Limiter.Take
  • Added opportunistic, overflow-safe bucket cleanup to Limiter
  • Fixed stop-phase budget sharing to ensure consistent lifecycle behavior

Documentation

  • Documented application lifecycle patterns and graceful shutdown process
  • Documented cost-limit rejection and bucket cleanup behavior
  • Unified version references to v0.0.1 in docs
  • Documented rate limiting and integrated examples in notes-starter

Refactoring

  • Delegated Serve logic to ServeContext for cleaner architecture

What's Next

The v0.0.2 release provides a solid foundation for production-grade applications with proper observability, graceful shutdown, and rate limiting. Future releases will expand on these capabilities.