Versioning and compatibility
This page says what a tanGO release promises not to break, and how a change that does break something is announced. It applies from v0.1.0 on.
What a version number means
- Patch releases (
0.1.x) never intentionally break the Covered API. Upgrading within a minor version is always safe. - Minor releases (
0.x.0) may break the Covered API, but only after a Deprecation window (below). - Security fixes are the one exception: a security fix may break the Covered API in any release, and its release notes say so explicitly.
tanGO makes no v1.0 stability promise yet.
The Covered API
Everything below is covered unless it is named under what is not covered.
-
Every exported identifier of every public package: the root
tangopackage,accounts,admin,auth,auth/jwt,db,i18n,mail,mail/mailtest,migration,model,observability,ratelimit,realtime,realtime/websocket,shellandtestdb. Exported means types, functions, methods, constants, variables, and the exported fields of exported structs. -
Every
tangoCLI command (newproject,newapp,run,check,makemigrations,openapi,migrate,migrate down,admin …,tuiandshell;tango helplists them), with its flags. -
The app-side flags a generated
main.godispatches, with their JSON output and exit-code expectations:-checkvalidates registration, route compilation, and app-contributed checks, then exits non-zero on failure.-tango-dump-modelsprints registered model metadata as JSON fortango makemigrations.-tango-statusprints registration/database/migration status as JSON fortango tui; it is read-only and does not createtango_migrations.-tango-openapiprints the OpenAPI document of the installedopenapiapp (behindtango openapi), or fails namingopenapi.Newwhen none is installed.-migrateapplies pending migrations;-migrate -downrolls back the most recently applied one.- the
-tango-admin-*flags (behindtango admin create,resetpassword,deactivate,grant-staff,revoke-staff,grant-superuserandrevoke-superuser) manage Admin accounts when the admin app is installed.
-
tango shell, as the shell guide describes it:- the command, its
-c EXPR,--readonlyand--helpoptions, and its exit codes:0for a session that ends cleanly,1for the first error of a non-interactive session or a project that fails to boot,2for a command line it does not understand, and130when Ctrl-C ends a running evaluation; shell.Runandshell.Options(ReadOnly,HelpersandDatabaseLabel, a display-only label the project derives from its database configuration after removing every credential) — the whole public surface of theshellpackage;- the eight helpers
Models,Describe,Get,List,Count,Create,UpdateandDelete, andContext(), with their documented arguments: models addressed asapp.Model, a query that is amap[string]anywithwhere(equality only),order,limitandoffset, and rows that aremap[string]anykeyed by Go field name holding the stored Go values; help(),exit()andquit(), theproject.qualifier through whichOptions.Helpersare called, andTANGO_SHELL_HISTORY=off.
- the command, its
-
Environment variables:
TANGO_DB_DSN(and itssqlite:///postgres:///mysql://grammar),TANGO_ADDR,TANGO_TEST_DSN,TANGO_SHELL_HISTORY(offkeeps the shell's history from being saved) andTANGO_ADMIN_PASSWORD(the password fortango admin createandresetpassword; empty counts as unset). -
storage:Store(Put,Stat,Open,Delete),PutOptions,Object,Info,NewKey,ValidKey,ETagFor,InspectandInspector,UploadwithUploadOptionsandUploadedFile,ServewithServeOptions, and the sentinel errorsErrNotFound,ErrExists,ErrTooLarge,ErrTypeNotAllowed,ErrInvalidKey,ErrInvalidRange,ErrInvalidOptions,ErrNotMultipart,ErrNoFile,ErrMultipleFilesandErrTooManyPartswithTooLargeErrorandTypeNotAllowedError;storage/local'sNew;storagetest.Run,FactoryandNewMemory; and, in its own module under the same policy,storage/s3'sNew,ConfigandCredentials. WhatServesends (the headers, the attachment default,nosniff, the status codes) and the rules that a key is generated and a content type is sniffed are covered; the layout of the filesstorage/localkeeps on disk is not. Native Google Cloud Storage is not part of v0.4.0. -
cache:Store(Get,Set,Delete) and its miss-is-not-an-error semantics,ValidKey,CheckTTL,MaxKeyLength,MaxTTL,Prefix,GetJSON,SetJSON,FetchJSONwithFetchOption,OnErrorand theOp…constants, the sentinel errorsErrInvalidKey,ErrInvalidTTL,ErrTooLargeandErrCorruptwithCorruptError,cache/local'sNewandConfig,cachetest.RunandFactory, and, in their own modules under the same policy,cache/redis'sNew,NewFromClientandConfigandcache/memcache'sNewandConfig.FetchJSON's fail-open rules are covered. The stored byte format and the key layout are the host's and not covered, and which Redis, Valkey and Memcached versions are supported is stated in the guide and changes as CI does. -
OpenAPI documents:
tango.Doc,tango.RouteDoc(its fields and their meaning),tango.DescribedRoute,RouteRegistry.Described,openapi.New,openapi.Dev,openapi.EnvVar,openapi.Options,openapi.Document,openapi.UIandswaggerui.New; the document's structure for routes, path and query parameters, bodies, responses,components/schemasand the bearer scheme; and theaccountsJSON endpoints' described shapes. A minor release may add members to the document and to the schemas. Not covered: the exact schema names of types you do not name yourself, Swagger UI's look and the version of its assets, and the operation ids of unnamed routes. -
The
tango:"…"struct tag grammar described in models and tags. -
Bounded strings: the
varchar=nandtexttags,model.FieldMeta.MaxLength,model.MaxVarcharLength,model.ErrInvalidFieldTag,db.ErrValueTooLonganddb.ValueTooLongError, the rune-counting rule, and the additiveLength,FromLengthandToLengthfields of generated migration steps. A barestringfield and atango:"text"field never change: they stay unbounded text, generate no migration and keep today's SQL on every v0.x release. -
JSON fields: the
jsontag,model.FieldMeta.JSON,db.ErrInvalidJSONanddb.InvalidJSONError. A JSON field is ordinary text: it adds no column type and no migration, and the generated migration-file format is unchanged. Whether a stored value is valid JSON is checked only byStore.CreateandStore.Update. -
The
tango_migrationstable: its name and its columns (app,name,applied_at). -
The generic View-error response: a View that returns an error, or panics behind
Recoverer, answers500with the JSON body{"error": "internal error"}. -
accounts' HTTP endpoints:GET/POST /accounts/register/,GET/POST /accounts/login/andPOST /accounts/logout/; withaccounts.WithMail, alsoGET/POST /accounts/password-reset/,GET/POST /accounts/password-reset/confirm/,GET/POST /accounts/verify/andPOST /accounts/verify/resend/, with their links'tokenquery parameter. On top of the endpoints, the only two JSON responsesaccountssends:- a rejected or missing CSRF token on a
POSTanswers403with a JSON object carrying an"error"string; - rate limiting answers
429with the same JSON shape and aRetry-Afterheader.
With
accounts.WithJSON, six more endpoints are covered:POST /accounts/api/register/,login/,password-reset/,password-reset/confirm/,verify/andverify/resend/. Their status codes, the members of their success bodies and thecodevalues of their errors ({"error", "code", "fields"?}) are covered; theerrortext is not. Responses are additive-only: a minor release may add a member, so a client ignores members it does not know. Requests are strict: unknown or duplicate members are rejected with400 invalid_body, so a new request member is never silently accepted. Every JSON response carriesCache-Control: no-store. The covered Go API isWithJSON,JSONConfig,JSONAuth,AuthGrant,RegistrationandFieldError, anddb.Store.InTx.Everything else
accountsanswers is an HTML page and not covered beyond its status: closed registration, for example, is a403HTML page, not JSON (POST /accounts/api/register/answers403withcode: "registration_closed"). A request with a method an endpoint doesn't register gets the router's bare405, which isn't part ofaccounts' contract. - a rejected or missing CSRF token on a
-
mail's wire format: a message with attachments ismultipart/mixed, with the text part first and each attachment base64-encoded; the text is quoted-printable UTF-8. Exact MIME boundaries and header order are not covered. -
Activity read side:
observability.Snapshotter,Snapshot,RequestSummary,JobSummary,Window,NewWindow,WithClockandMulti, with the documented limits ofWindow(60 one-minute buckets, global aggregates, no route, method, status code or job name retained). Theadmindashboard's types (DashboardConfig,DashboardStatusand its state constants,WithDashboard) are covered the same way; how the dashboard page looks is best-effort like the rest of admin's rendering. -
Observability names, as listed in the observability guide:
- the log event names
tango.http.view_error,tango.http.panic,tango.http.access,tango.scheduler.job_failed,tango.http.request_id_generation_failed, andaccounts'tango.accounts.mail_failedandtango.accounts.mail_dropped; - the metric names
tango_http_request_duration_seconds,tango_scheduler_job_invocations_totalandtango_realtime_room_events_total; - their attribute keys:
route,method,request_id,status,duration_seconds,error,recovered,stack,job,panicked,event,outcomeandpurpose; - their enumerated values: a scheduler job's
outcomeissuccess,error,canceledorpanic; a realtime room event'seventisjoin,leaveordispatch, and itsoutcomeissuccessorerror; anaccountsmail event'spurposeispassword_resetoremail_verification; - the
routevalue"(unmatched)", which every Unmatched request reports underMiddlewareScopeAll.
- the log event names
A new release may add to any of these: a new function, a new optional flag, a new JSON key, a new attribute. Code and tools that ignore what they don't recognise keep working.
Build structs with keyed literals
Keyed struct literals are the supported way to build tanGO's structs:
config := tango.Config{InstalledApps: apps, Addr: ":8000"} // supported
config := tango.Config{apps, ":8000", nil} // unsupported
A minor release may add a field to any covered struct, which breaks an unkeyed literal. This is the rule Go's own compatibility promise uses. go vet reports unkeyed literals of imported struct types by default.
What is not covered
-
internal/. Go already prevents importing it. -
The migration types and functions that exist for generated files and the CLI, not for hand-written application code:
migration.Step,migration.Column, and the step structsCreateTable,DropTable,AddColumn,DropColumn,AlterColumnUnique,CreateIndex,DropIndex,RenameColumn,RenameTableandAlterColumnType;migration.Model,SchemaState,TableStateandColumnState;- the diff and replay surface:
Diff,DiffModels,Replay,Rename,ModelsFromMeta,ErrInvalidRenameandErrUnsupportedChange; migration.ApplyStep.
These may change in any minor release, except as the Generated-file contract protects them. Applications apply migrations with
ApplyPendingandRollbackLast, which are covered, as areMigration,EnsureTrackingTable,AppliedMigrations,IsMissingTrackingTable,MigrationKey,ErrIrreversibleMigrationandErrNoAppliedMigrations. -
Best-effort admin extensibility:
admin.Widget,admin.PreviewWidgetand the types they use (FieldContextand its methods,FieldValues,SelectOption), theadmin.Optionspresentation fields (Widgets,Labels,HelpText,ReadOnly,FieldOrder), the built-inadmin.Textarea, the presentation helpersadmin.ByteSizeandadmin.ShortHash, andadmin.Branding/admin.WithBranding. Admin's rendering and theme are still settling, so these may change in any minor release without a Deprecation window, though the changelog always lists the change. -
The admin surfaces that are covered, inside the admin's otherwise best-effort area:
admin.Options.Filters;admin.Options.Display,admin.DisplayFuncandadmin.Display;admin.JSON();admin.Markdown(...),admin.MarkdownOption,admin.WithMarkdownPreviewandadmin.MarkdownPreview; andadmin.FieldError. Their Go signatures and documented behaviour follow the Covered API rules. What they render does not: the admin's HTML and CSS, the filter query-string keys, the preview route's path and the preview's JSON bodies are presentation details that may change in any minor release. -
Admin and
accountspresentation: their HTML, templates, CSS (including the light and dark theme, the colour token names and the theme switch's endpoint and cookie), presentation redirects, and admin URLs. A renamed model, for example, changes its admin URL. -
tango tui's screen: its layout, wording, colours and key bindings. What is covered is that the command exists; that the interactive dashboard requires a terminal; that actions that change the database ask for explicit confirmation first; and that it runs the same supported operations astango run,tango migrateandtango migrate down. The interactive dashboard requires a terminal. In non-interactive environments,tango tuiprints status only; use the dedicated CLI commands to run the server or change migrations. See thetango tuiguide. -
What
tango shellruns on, and how it looks: the interpreter (Yaegi, pinned at v0.16.1) and so the exact subset of Go it can run, which a release may change; the formatting of results beyond sorted map keys and one row per line; the wording of its errors, hints, help text and startup banner; where its history file lives and its format; what happens when a value of a project's own type crosses into a helper (only plain values, maps, slices anderrorare supported); and theproject/andshell/filestango newprojectwrites, which are your code, not a library API. -
The wording of error and log messages. Match errors with
errors.Isagainst the exported sentinel errors, which are covered, never against their text.
How defaults change
A change to how an existing app behaves, such as a new default, ships as an opt-in first, and new projects from tango newproject opt in. An app that doesn't opt in keeps its behaviour. A default changes later only after a minor release of notice in the changelog. Where a setting's zero value means "the framework's default" (like Config.MiddlewareScope), setting it explicitly keeps your choice whatever the default becomes. The one exception is a security fix, which may change a default at once and is called out in the release notes.
Deprecation
A deprecated part of the Covered API is announced in three places:
- a Go
// Deprecated:comment on the identifier; - a Deprecated entry in the changelog;
- the release notes of the release that deprecates it.
It then keeps working for at least one minor release: something deprecated in 0.2 is removed no earlier than 0.3. A behaviour or flag change with no Go symbol to mark is announced in the changelog one minor release before it lands.
The Generated-file contract
A migration file written by any released tango makemigrations keeps working on every later v0.x release: it compiles, later tango makemigrations runs read its // tango:migration-json header, and it applies and rolls back. This holds even though the step types it uses are otherwise not covered:
- the fields of
migration.Migration(App,Name,Up,Down,Reversible), ofColumn, and of every step struct the generator writes may gain new fields, but a field a released generator wrote is never removed or renamed; - the header's kinds and keys keep their meaning.
Steps you write by hand, and field combinations the generator never writes, are not protected. tanGO's test suite checks the contract against migration files generated by every released version, on SQLite, PostgreSQL and MySQL.
Changes to tanGO's own models
The admin and accounts models reach your database through your own tango makemigrations, so a change to one of them is a change to the Covered API:
- it ships only in a minor release, never a patch;
- the changelog lists it under Changed, with instructions to run
tango makemigrationsand apply the result; - it is always something
tango makemigrationsgenerates without--renameor--allow-drop: an added field or a Widening type change; - renaming or removing one of their fields goes through the Deprecation window: the replacement arrives in one minor release, and the original is removed no earlier than the next.
How a release is made
Every release has a dated section in the changelog, used as its release notes, and is published only after CI passed on the exact commit being tagged. The maintainer's checklist is RELEASING.md.