Fluid Forge Docs Baseline: CLI 0.17.0
Release dates: 0.16.3 on September 26, 0.16.4 on September 27, and 0.16.5, 0.16.6, 0.16.7 and 0.17.0 on September 28, 2026. Status: Superseded by 0.18.0, whose page also covers the current release, 0.18.1. Supersedes 0.16.0.
This baseline covers six CLI releases, documented together because none had a docs pass of its own. To move a project from an older release, use the upgrade guide.
Headline
One contract now deploys to AWS and to Google Cloud through its --env overlays, and is governed the same on both.
- Each policy is one contract field, enforced natively on each cloud. Retention, encryption at rest with a per-product key, column restrictions, masking and grants reach AWS (
0.16.5–0.16.7) and Google Cloud (0.17.0), instead of being declared and dropped.fluid verifychecks retention, encryption and column restrictions, and checks masking on Glue, Athena and BigQuery tables. On Google Cloud,fluid verifydoes not read dataset-level grants back; on AWS it checks the Lake Formation column grants of an expose that declares column restrictions. - The two clouds keep two states. A product applied with
--env awsand--env gcpused to share one OpenTofu state, so each cloud's plan read the other's resources as orphans to destroy. - The generated 11-stage pipeline runs as generated (
0.16.3), and its gates check what they claim: a tampered plan, a mismatched mode or env, and a live target that drifted all fail where they used to pass. - A chained product reads what its
consumes[]names (0.16.5), on DuckDB, from S3 or from BigQuery (0.17.0).
What has been run live
The forge-cli changelog for 0.17.0 measured the GCP work against moto and a BigQuery emulator, and left the real-cloud run open. On 4 October 2026, on 0.18.0, a demo lab applied two lineage chains of eleven products to real Google Cloud with fluid apply --env gcp, from generated Jenkins pipelines, as a deploy service account reached by Workload Identity Federation. In that run:
- each apply created its dataset, key ring and key, its table with daily partitions that expire after the retention, its dataset IAM members, and a policy tag on each restricted column;
- each build passed
fluid verifyagainst the live tables, including retention, encryption (Cloud KMS) and column restrictions (Data Catalog policy tags); - silver and gold products built on GCP by reading their upstreams from BigQuery;
- querying as each persona, an analyst was refused each restricted column, a steward could read it, and a principal with no dataset grant was refused the table. BigQuery's refusal reads:
User has neither fine-grained reader nor masked get permission to get data protected by policy tag "<taxonomy> : <tag>" on column <project>.<dataset>.<table>.<column>.
The same run found the drift-gate failure on 0.18.0 that 0.18.1 fixes.
Not proven: the Lake Formation half against a real AWS account as 0.17.0 derives it from columnRestrictions. The grant shape it emits (excluded columns beside wildcard) was applied and enforced on a real account from 0.16.6, written by hand in the overlay. See Governance parity.
Who should upgrade
Anyone who deploys one contract to more than one cloud. Anyone on Google Cloud whose contract declares retention, encryption, column restrictions or grants. Anyone on AWS with Lake Formation grants, lifecycle.retention, binding.encryption or policy.privacy.masking. Anyone who runs a pipeline generated by fluid generate ci, or syncs DAGs with fluid schedule-sync. Anyone who publishes to a Command Center.
Upgrade checklist
From 0.16.2. Steps 1–7 come from 0.16.3, 8–11 from 0.16.5, and 12–16 from 0.17.0. The upgrade guide has a check for each.
- Relative local outputs land under the contract's directory, not the directory you ran the command from. Move anything that reads the old location.
fluid verify --strictfails a local output whose columns differ from the declared schema. It used to downgrade that to a warning.fluid apply --build-idwith a mode that runs no build is refused, and so is applying aplan.jsonin a different mode from the one it was planned in.- The drift gate can fire.
fluid diff --exit-on-driftcompares the live target: local files, DuckDB, Glue and BigQuery tables. Pass--last-appliedto tell a contract change from drift. A target not created yet is not drift. - Command Center products are private unless the contract's classification is
public. - Lake Formation bucket policies no longer name grantees in the same account. Those grantees could read the data straight from S3, around Lake Formation. To restore the old output, set
binding.governance.lakeFormation.bucketPolicy: all-grantees(fluid-schema0.7.6). - Regenerate committed pipelines with
fluid generate ci. A generated Jenkinsfile installs thedata-product-forgeversion that generated it, so a pipeline generated before the upgrade keeps running the old CLI. - A
consumes[]entry that the build's SQL reads must resolve on the DuckDB embedded-SQL path: from the nearestfluid.workspace.yaml, aFLUID_UPSTREAM_CONTRACTSroot, or an explicit input of the same name. It used to be skipped with a warning; it now fails withConsumesResolutionError. Entries the SQL does not read stay lineage only. fluid apply plan.json --env <other>fails withplan_env_mismatch.fluid plan --env Xrecords the env in the plan, under its digest.- With
AWS_ENDPOINT_URLorAWS_ENDPOINT_URL_S3set, a DuckDB S3 secret that fails stops the run (ObjectStoreEndpointError), instead of sending reads and writes to AWS. - Masked columns land treated. The DuckDB runner now applies
exposes[].policy.privacy.masking[].hashneeds a salt of at least 16 bytes inFLUID_PII_HASH_SECRET(or the variableparams.saltEnvnames),tokenizea key of at least 32 bytes, andencrypta base64 key; an unset secret refuses the build.k_anonymityis refused. A treated column lands as a string, so declare it as one. - Run
fluid schedule-synconce to retire each env's old DAGs. DAGs move from<product-id>/with dag id<product>__<build>to<product-id>__<env>/with dag id<product>__<env>__<build>. Left in place, the old DAG runs beside the new one, and both apply the same product against the same state. - Remote state moves to a per-provider key on the first apply:
fluid/<id>/<provider>/terraform.tfstate. The apply copies the state with OpenTofu's own migration and leaves the old object in place. A key shared with another cloud's resources is refused (state_shared_with_another_provider). - GCP dataset grants stop being authoritative. They become
google_bigquery_dataset_iam_memberresources. The first apply revokes, once, the old access entries that no grant of the contract covers, and prints them. --env <name>with no overlay is refused when the workspace'sexpected-environmentslists that env for the product. It used to run the base contract.- A GCP binding with no region is refused, instead of landing in
US.
Behaviour changes that can newly fail
Steps 2, 3, 4, 8, 9, 10, 11, 15 and 16 above can newly fail with no contract change. Steps 13 and 14 can also refuse the first apply (see the upgrade guide), and steps 12 to 14 change what exists in your scheduler, your state bucket and your datasets on the first run after the upgrade.
What changed in 0.17.0
Governance parity on Google Cloud
Each field in this table is enforced natively on both clouds and checked by fluid verify:
| Contract field | AWS | Google Cloud |
|---|---|---|
lifecycle.retention with lifecycle.expire: true | S3 lifecycle rule on the expose's prefix | daily partitions that expire (not a table TTL) |
binding.encryption.kms: product | a rotated KMS key and alias per product and bucket | a key ring and a 90-day rotating key per product and dataset, granted to the BigQuery service agent |
policy.authz.columnRestrictions | each Lake Formation grant's excluded columns | a Data Catalog taxonomy with policy tags and fine-grained access control |
lifecycle.expire, binding.encryption and binding.principals are in fluid-schema 0.7.6 only, so a contract needs fluidVersion: "0.7.6" to use them. Governance parity has the full table, including what fluid verify checks on each cloud.
binding.principals. An overlay maps the contract's logical principals to the identities they are on that cloud: one, a list, or []. With the block present, an unmapped principal is refused. On Google Cloud a placeholder principal (a reserved domain, or not an IAM member) is refused either way, because BigQuery refuses an access entry for an identity that does not exist.
Embedded-SQL builds read and land BigQuery
A consumes[] entry whose upstream is a gcp bigquery_table resolves to that table, and a build whose expose is one loads its result with a BigQuery load job (WRITE_TRUNCATE, CREATE_NEVER), failing on a short load. Naive timestamps load as UTC TIMESTAMP. fluid verify checks a BigQuery table's row_count against the build's run records, and masked columns, as it already did on Glue and Athena.
One contract on two clouds
Per-provider state. See step 13. When the gcp apply finds the aws state at the old key, it leaves it alone. On the 4 October 2026 run, that first gcp apply printed a line of this form, where
<old-state-location>iss3://<bucket>/<key>orgcs://<bucket>/<prefix>:state move: <old-state-location> holds the aws provider's state (hashicorp/aws), not this provider's; left in placeand wrote the gcp state to
fluid/<id>/gcp/terraform.tfstate.DAGs per env. The env is in the dag id and the schedule scope, so the aws and gcp DAGs of one product no longer collide. See step 12.
Overlays are enforced where the workspace expects them. See step 15.
Sovereignty fails closed on GCP. A binding with no region is refused,
fluid generate iacchecks the region,location.locationis checked likelocation.region, and stage 6 of a generated pipeline runsfluid plan --check-sovereignty.
fluid apply reports each run to the Command Center
When the Command Center publish configuration is present (FLUID_CC_ENDPOINT, an API key or bearer token, and an organization from FLUID_CC_ORG_ID or fluid.config.yaml), each fluid apply records a run: the product id, contract version and hash, env, provider, mode, state location, change counts, resource addresses and, for a build mode, each build's result. No credential, environment value or OpenTofu output is sent. Reporting is best effort: a Command Center that is down costs a warning line, never the apply's exit code. Set FLUID_COMMAND_CENTER_ENABLED=false to turn it off.
fluid publish upserts one Command Center product per contract id, so publishing from --env aws and from --env gcp leaves the platform and location of whichever ran last. A generated Jenkinsfile has its publish stage off by default. Pass --publish-stage-default to turn it on for the env that should publish. Only the Jenkins template reads this flag.
Fixed in 0.17.0
- Stage 8 of a gcp pipeline no longer crashes:
fluid policy-applyraised aTypeErrorfrom its own log call. - A BigQuery load runs in the table's location, never at a guessed
US.
What changed in 0.16.5–0.16.7
The policies an AWS contract declares reach AWS.
- Retention and encryption at rest reach S3 (
0.16.5).lifecycle.expire: trueturnslifecycle.retentioninto an S3 lifecycle rule;binding.encryption.kms(product,none,alias/<name>or a key ARN) gives SSE-KMS with a bucket key. Both are written only on a bucket the product owns; a bucket that already has rules or SSE is adopted.fluid verifyreads the live rules and key and fails on a difference. - Masking is applied when the data lands (
0.16.5). See step 11. The local file, the S3 object, the BigQuery staging file and the dead-letter queue all hold treated values, andfluid verifyfails a masked column that landed in cleartext. - Chained products (
0.16.5). Eachconsumes[]entry the SQL reads becomes a view named by itsexposeId, over the upstream contract's binding under the same--envoverlay.AWS_ENDPOINT_URL_S3/AWS_ENDPOINT_URLreach DuckDB, so a chain runs against MinIO or moto. fluid diffreads the apply's OpenTofu state (0.16.5). It runstofu plan -detailed-exitcodeagainst the apply's own state, so a setting changed by hand or a table deleted by hand fails--exit-on-drift. New flags:--state-backend,--workspace-dir,--no-state-drift,--ensure-opentofu.fluid generate ci --fluid-env-default <env>(0.16.5) sets the env a build on the defaults runs in, instead ofdev.fluid contract-testsruns its comparison (0.16.5). It returned "compatible" for every contract.- Lake Formation grants that hide columns plan and apply (
0.16.6,0.16.7). A grant withexcludedColumnssetswildcard = true, and a column-limited grant carriesSELECTonly, which is what Lake Formation accepts. A column limit besideALTER,DROP,INSERT,DELETEorALLis refused when the module is emitted, and so are excluded columns the schema does not have.
What changed in 0.16.3 and 0.16.4
The generated 11-stage pipeline runs as generated.
- The generated Jenkinsfile runs end to end (
0.16.3).CONTRACTdefaults to the contract you generated from. Stage 0 installs into a workspace venv and pinsdata-product-forgeto the generating version with the extras the contract needs (--fluid-package-specoverrides it). Every parameter's default is also its shell fallback, so a first build, or a build after a Jenkins restart, is a correct build. Stage 1 bundles with--env, and later stages read the bundle. - The structural gates check what they claim (
0.16.3). Plan digest and mode are checked before any build. The bundle records its source contract and env, and a stage asked for a different env refuses it. fluid verifychecks an S3 + Glue binding (0.16.3): the Glue table's columns, and an AthenaCOUNT(*)against the rows the build landed.- Scheduled builds get an Airflow 3 DAG that runs
fluid apply(0.16.3), andfluid schedule-syncdeletes only within the product's own folder. FLUID_STATE_BACKENDsets the default forfluid apply --state-backend(0.16.3); a bucket-only value keys each contract's state apart.- Command Center publishing (
0.16.3).fluid publishsendsX-Organization-IdfromFLUID_CC_ORG_ID(or the caller's single organization, or an organization slug influid.config.yaml), takes--env, records each contract version, and keeps the contract's classification.--dry-runshows the body it would send. fluid verifyandfluid difffind a local file whose path uses{{ env.NAME }}(0.16.4), where the build wrote it.- New
fluid generate cioptions (0.16.3):--apply-mode-default,--schedule-sync-default,--scheduler-default,--scheduler-destination-default,--diff-last-appliedand--fluid-package-spec.
See also
- Upgrade guide
0.18.0and0.18.1release notes0.16.0release notes- One contract, two clouds, Per-environment overlays, Switch clouds
- OpenTofu state, Governance parity, The Command Center, Federated upstreams
fluid apply,fluid diff,fluid verify,fluid schedule-sync- 11-stage pipeline walkthrough
- forge-cli CHANGELOG