# Publish to Entropy Data

Make your data products discoverable on a data product platform.

Source: https://learn.datacontract.com/en/publish/

YAML files in Git work well for a single team. But how do *other* teams discover your data products, browse the contracts, and request access?
That's what a **data product platform** is for.
In this exercise, you publish everything from Parts A and B to [Entropy Data](https://entropy-data.com) with the [Entropy Data CLI](https://github.com/entropy-data/entropy-data-cli).

**You will learn:**
- Connect the Entropy Data CLI to a cloud organization or a local Community Edition
- Publish teams, data contracts, and data products
- Model the dependency between two data products as an access agreement
- Publish test results to show whether a contract is upheld

> **Optional part**
>
> Part D is optional and uses a commercial platform with a free tier. The free **Community Edition** runs fully locally. Everything you built so far works without it.

## Get access

### Create an organization

Pick **one** option.

**Option 1: Cloud.** Go to [app.entropy-data.com](https://app.entropy-data.com), create an account, and set up an organization named `tutorial-<yourname>`, e.g. `tutorial-simon`.
Names are unique across the platform. Use lowercase letters, digits, and hyphens only.
In the organization settings, create an **API key** with organization write permissions.

**Option 2: Community Edition, locally.** Start it in Docker and run the setup script. It creates an account, an organization named `acme`, and an API key:

macOS / Linux:

```bash
docker compose -f entropy-data-ce/docker-compose.yaml up -d
./scripts/setup-entropy-data-ce.sh
```

Windows (PowerShell):

```powershell
docker compose -f entropy-data-ce/docker-compose.yaml up -d
powershell -ExecutionPolicy Bypass -File scripts\setup-entropy-data-ce.ps1
```

Output:

```text
…
Creating account workshop@example.com ...
Logging in ...
Creating organization acme ...
Creating organization API key ...
Writing ENTROPY_DATA_API_KEY and ENTROPY_DATA_HOST to .env ...

Done. Log in at http://localhost:8081 with workshop@example.com / workshop (organization: acme)
Verify the CLI connection with: entropy-data connection test
```

Log in at [http://localhost:8081](http://localhost:8081) with `workshop@example.com` / `workshop`.
Below, use `acme` wherever it says `tutorial-<yourname>`.

### Configure the connection

The install script from [Setup](https://learn.datacontract.com/en/setup/) already installed the Entropy Data CLI. Check:

macOS / Linux:

```bash
entropy-data --version
```

Windows (PowerShell):

```powershell
entropy-data --version
```

Output:

```text
entropy-data 0.3.13
```

**Cloud:** set your API key as an environment variable in your terminal:

macOS / Linux:

```bash
export ENTROPY_DATA_API_KEY=ed_...
export ENTROPY_DATA_HOST=https://api.entropy-data.com
```

Windows (PowerShell):

```powershell
$env:ENTROPY_DATA_API_KEY = "ed_..."
$env:ENTROPY_DATA_HOST = "https://api.entropy-data.com"
```

> **Never commit your API key**
>
> Your fork is public, and `.env` is tracked in Git. Anything you push there is visible to everyone and stays in the history.
> So set the key in your shell instead. Shell variables take precedence over the placeholders in `.env`.
> Repeat these lines in every new terminal.

**Community Edition:** the setup script wrote the key and `ENTROPY_DATA_HOST=http://localhost:8081` into `.env`. Keep it out of Git anyway: check `git status` before you commit and undo the change with `git checkout .env`. Never run `git add .env`.

Test the connection:

macOS / Linux:

```bash
entropy-data connection test
```

Windows (PowerShell):

```powershell
entropy-data connection test
```

Output:

```text
Connection successful.
```

## Publish

### Create your teams

Contracts and data products name a team as owner. The team must exist before you publish anything that references it:

macOS / Linux:

```bash
cat <<EOF | entropy-data teams put order_data_team --file -
id: order_data_team
name: Order Data Team
type: team
description: Owns the orders data
EOF

cat <<EOF | entropy-data teams put purchasing_analytics_team --file -
id: purchasing_analytics_team
name: Purchasing Analytics Team
type: team
description: Builds analytical data products for purchasing
EOF

entropy-data teams list
```

Windows (PowerShell):

```powershell
@'
id: order_data_team
name: Order Data Team
type: team
description: Owns the orders data
'@ | entropy-data teams put order_data_team --file -

@'
id: purchasing_analytics_team
name: Purchasing Analytics Team
type: team
description: Builds analytical data products for purchasing
'@ | entropy-data teams put purchasing_analytics_team --file -

entropy-data teams list
```

Output:

```text
Team 'order_data_team' saved.
Team 'purchasing_analytics_team' saved.
                                   teams (page 0)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
┃ ID                        ┃ Name                      ┃ Type             ┃ Parent ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
│ governance-group          │ Governance Group          │ Governance Group │        │
│ platform-team             │ Platform Team             │ Platform Team    │        │
│ order_data_team           │ Order Data Team           │ team             │        │
│ purchasing_analytics_team │ Purchasing Analytics Team │ team             │        │
└───────────────────────────┴───────────────────────────┴──────────────────┴────────┘
```

The `team.name` in your ODCS and ODPS files must match the team ID, e.g. `order_data_team`.

### Publish your data contracts

The ID argument must match the `id` field in each contract:

macOS / Linux:

```bash
entropy-data datacontracts put orders_v1 --file orders_v1.odcs.yaml
entropy-data datacontracts put orders_v2 --file orders_v2.odcs.yaml
entropy-data datacontracts put sku_sales_per_year --file sku_sales_per_year.odcs.yaml
entropy-data datacontracts put orders_v2_consumer_sku_sales --file orders_v2.consumer_sku_sales.odcs.yaml

entropy-data datacontracts list
```

Windows (PowerShell):

```powershell
entropy-data datacontracts put orders_v1 --file orders_v1.odcs.yaml
entropy-data datacontracts put orders_v2 --file orders_v2.odcs.yaml
entropy-data datacontracts put sku_sales_per_year --file sku_sales_per_year.odcs.yaml
entropy-data datacontracts put orders_v2_consumer_sku_sales --file orders_v2.consumer_sku_sales.odcs.yaml

entropy-data datacontracts list
```

Output:

```text
Data contract 'orders_v1' saved.
Open https://app.entropy-data.com/tutorial-simon/datacontracts/orders_v1
Data contract 'orders_v2' saved.
Open https://app.entropy-data.com/tutorial-simon/datacontracts/orders_v2
Data contract 'sku_sales_per_year' saved.
Open https://app.entropy-data.com/tutorial-simon/datacontracts/sku_sales_per_year
Data contract 'orders_v2_consumer_sku_sales' saved.
Open https://app.entropy-data.com/tutorial-simon/datacontracts/orders_v2_consumer_sku_sales
                                  datacontracts (page 0)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ID                           ┃ Title              ┃ Version ┃ Owner                     ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ orders_v1                    │ Orders             │ 1.0.0   │ order_data_team           │
│ orders_v2                    │ Orders             │ 2.0.0   │ order_data_team           │
│ sku_sales_per_year           │ SKU Sales per Year │ 1.0.0   │ purchasing_analytics_team │
│ orders_v2_consumer_sku_sales │ Orders (SKU Sales) │ 2.0.0   │ purchasing_analytics_team │
└──────────────────────────────┴────────────────────┴─────────┴───────────────────────────┘
```

### Publish your data products

Entropy Data supports ODPS natively, so you publish the files as they are.
The ID argument must match the `id` field in the file:

macOS / Linux:

```bash
entropy-data dataproducts put orders --file orders.odps.yaml
entropy-data dataproducts put sku_sales --file sku_sales_per_year.odps.yaml

entropy-data dataproducts list
```

Windows (PowerShell):

```powershell
entropy-data dataproducts put orders --file orders.odps.yaml
entropy-data dataproducts put sku_sales --file sku_sales_per_year.odps.yaml

entropy-data dataproducts list
```

Output:

```text
Data product 'orders' saved.
Data product 'sku_sales' saved.
                    dataproducts (page 0)
┏━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ID        ┃ Title     ┃ Status ┃ Owner                     ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ orders    │ Orders    │ active │ order_data_team           │
│ sku_sales │ SKU Sales │ draft  │ purchasing_analytics_team │
└───────────┴───────────┴────────┴───────────────────────────┘
```

> **Workaround**
>
> Due to a current bug in Entropy Data, publishing fails if the ODPS file contains `inputPorts`. Remove the `inputPorts` section from `sku_sales_per_year.odps.yaml` right before you publish. The next step models the dependency another way.

> **Alternative: the Data Product CLI**
>
> `dataproduct publish` validates the file against the ODPS schema and publishes it in one go. It reads `ENTROPY_DATA_API_KEY` and `ENTROPY_DATA_HOST` the same way and takes the ID from the file:
>
> macOS / Linux:
>
> ```bash
> dataproduct publish orders.odps.yaml
> ```
>
> Windows (PowerShell):
>
> ```powershell
> dataproduct publish orders.odps.yaml
> ```
>
> Output:
>
> ```text
> ✅ Published data product successfully
> ```

### Connect the data products

On the platform, the dependency is an **access agreement**: SKU Sales consumes the `orders_v2` output port of Orders. Create it directly as approved:

macOS / Linux:

```bash
cat <<EOF | entropy-data access put sku_sales_consumes_orders --file -
dataUsageAgreementSpecification: 0.0.1
id: sku_sales_consumes_orders
info:
  purpose: SKU Sales aggregates orders and line items per SKU and year for the purchasing team
  status: approved
  startDate: "2026-01-01"
provider:
  dataProductId: orders
  outputPortId: orders_v2
  dataContractId: orders_v2
consumer:
  dataProductId: sku_sales
EOF
```

Windows (PowerShell):

```powershell
@'
dataUsageAgreementSpecification: 0.0.1
id: sku_sales_consumes_orders
info:
  purpose: SKU Sales aggregates orders and line items per SKU and year for the purchasing team
  status: approved
  startDate: "2026-01-01"
provider:
  dataProductId: orders
  outputPortId: orders_v2
  dataContractId: orders_v2
consumer:
  dataProductId: sku_sales
'@ | entropy-data access put sku_sales_consumes_orders --file -
```

Output:

```text
Access agreement 'sku_sales_consumes_orders' saved.
```

With status `approved` and a `startDate` in the past, the agreement is active.

> **Why an access agreement?**
>
> An input port says *"I read this data."* An access agreement adds *"…and the owner approved it, for this purpose."*
> So the owner of Orders knows every consumer and can warn them before changes ship.

## Explore

### Explore the platform

Open the web UI ([app.entropy-data.com](https://app.entropy-data.com) or [localhost:8081](http://localhost:8081)):

- Find your two data products and their output ports.
- Open the `sku_sales_per_year` contract and compare it with the Data Contract Editor.
- Follow the access agreement from SKU Sales to Orders. The dependency from [Design Contract-First](https://learn.datacontract.com/en/contract-first/) is now a link in the data product map.
- How would someone find a data product about SKUs without knowing it exists?

![The data product list: Orders with two output ports, SKU Sales with one](https://learn.datacontract.com/screenshots/publish-data-products.webp)

![SKU Sales consumes Orders: the access agreement as a link in the data product map](https://learn.datacontract.com/screenshots/publish-sku-sales-product.webp)

### Publish test results

The platform shows whether a contract is *currently* upheld, if you send it test results.
Run your tests with `--publish` and your host's test results endpoint:

Cloud:

macOS / Linux:

```bash
datacontract test sku_sales_per_year.odcs.yaml --publish https://api.entropy-data.com/api/test-results
```

Windows (PowerShell):

```powershell
datacontract test sku_sales_per_year.odcs.yaml --publish https://api.entropy-data.com/api/test-results
```

Output:

```text
Testing sku_sales_per_year.odcs.yaml
🚀 Open https://app.entropy-data.com/tutorial-simon/datacontracts/sku_sales_per_year/servers/postgres/checks
Server: postgres (type=postgres, host=localhost, port=5433, database=workshop, schema=analytics)
╭────────┬──────────────────────────────────────────────────┬─────────────┬─────────╮
│ Result │ Check                                            │ Field       │ Details │
├────────┼──────────────────────────────────────────────────┼─────────────┼─────────┤
│ passed │ Ensure the view has data                         │             │         │
│ passed │ Check that field 'order_count' is present        │ order_count │         │
│ …      │                                                  │             │         │
│ passed │ Ensure year is plausible                         │ year        │         │
╰────────┴──────────────────────────────────────────────────┴─────────────┴─────────╯
🟢 data contract is valid. Run 15 checks. Took 0.58 seconds.
```

Community Edition:

macOS / Linux:

```bash
datacontract test sku_sales_per_year.odcs.yaml --publish http://localhost:8081/api/test-results
```

Windows (PowerShell):

```powershell
datacontract test sku_sales_per_year.odcs.yaml --publish http://localhost:8081/api/test-results
```

The CLI sends your API key only if the URL belongs to the host in `ENTROPY_DATA_HOST`. Find the results on the contract page in the UI.

![The contract page with the published test results under Data Quality](https://learn.datacontract.com/screenshots/publish-contract-test-results.webp)

**Solution: publish.sh**

```bash title=publish.sh
#!/bin/bash
# Publishes all workshop artifacts to Entropy Data.
# Requires ENTROPY_DATA_API_KEY and ENTROPY_DATA_HOST (set via .env in the repository root).
set -e
cd "$(dirname "$0")"
if [ -f ../../.env ]; then set -a; . ../../.env; set +a; fi

entropy-data connection test

# teams must exist before contracts/products that reference them as owner
cat <<EOF | entropy-data teams put order_data_team --file -
id: order_data_team
name: Order Data Team
type: team
description: Owns the orders data
EOF

cat <<EOF | entropy-data teams put purchasing_analytics_team --file -
id: purchasing_analytics_team
name: Purchasing Analytics Team
type: team
description: Builds analytical data products for purchasing
EOF

entropy-data teams list

entropy-data datacontracts put orders_v1 --file ../exercise1/orders_v1.odcs.yaml
entropy-data datacontracts put orders_v2 --file ../exercise2/orders_v2.odcs.yaml
entropy-data datacontracts put sku_sales_per_year --file ../exercise4/sku_sales_per_year.odcs.yaml
entropy-data datacontracts put orders_v2_consumer_sku_sales --file ../exercise6/orders_v2.consumer_sku_sales.odcs.yaml

entropy-data dataproducts put orders --file ../exercise3/orders.odps.yaml

# WORKAROUND for a bug in Entropy Data: ODPS input ports currently (wrongly) require a
# sourceSystemId customProperty - strip the inputPorts section right before publishing
# and model the dependency as an approved access agreement instead.
sed '/^inputPorts:/,/^outputPorts:/{/^outputPorts:/!d;}' ../exercise4/sku_sales_per_year.odps.yaml \
  | entropy-data dataproducts put sku_sales --file -

# status approved + a startDate in the past makes the agreement active (the active flag is computed)
cat <<EOF | entropy-data access put sku_sales_consumes_orders --file -
dataUsageAgreementSpecification: 0.0.1
id: sku_sales_consumes_orders
info:
  purpose: SKU Sales aggregates orders and line items per SKU and year for the purchasing team
  status: approved
  startDate: "2026-01-01"
provider:
  dataProductId: orders
  outputPortId: orders_v2
  dataContractId: orders_v2
consumer:
  dataProductId: sku_sales
EOF

# re-run all contract tests and publish the results to the configured Entropy Data host
export DATACONTRACT_POSTGRES_USERNAME=workshop
export DATACONTRACT_POSTGRES_PASSWORD=workshop
datacontract test ../exercise1/orders_v1.odcs.yaml --publish "${ENTROPY_DATA_HOST:-https://api.entropy-data.com}/api/test-results"
datacontract test ../exercise2/orders_v2.odcs.yaml --publish "${ENTROPY_DATA_HOST:-https://api.entropy-data.com}/api/test-results"
datacontract test ../exercise4/sku_sales_per_year.odcs.yaml --publish "${ENTROPY_DATA_HOST:-https://api.entropy-data.com}/api/test-results"
datacontract test ../exercise6/orders_v2.consumer_sku_sales.odcs.yaml --publish "${ENTROPY_DATA_HOST:-https://api.entropy-data.com}/api/test-results"

entropy-data datacontracts list
entropy-data dataproducts list
entropy-data access list
```

**Quick check:** Where should the Entropy Data API key go when you work in a public fork?

- Into the tracked .env file, it is meant for credentials
- Into an environment variable in your shell, or a GitHub Actions secret in CI (correct)
- Into the data contract, next to the server
- Into the .env file, but delete it again before you push

Right. Anything committed to a public repository is public, including the history. Keep secrets in your environment or your CI system's secret store.

## Bonus

- **Publish test results from CI.** In your fork on GitHub, go to **Settings → Secrets and variables → Actions** and add a repository secret `ENTROPY_DATA_API_KEY`. In the workflow from [CI/CD with GitHub Actions](https://learn.datacontract.com/en/ci-cd/), pass it to the test step and add `--publish`:

  ```yaml
  - name: Test data contracts
    env:
      ENTROPY_DATA_API_KEY: ${{ secrets.ENTROPY_DATA_API_KEY }}
      ENTROPY_DATA_HOST: https://api.entropy-data.com
    run: datacontract ci *.odcs.yaml --publish https://api.entropy-data.com/api/test-results
  ```

  This works only with the cloud: GitHub's runners can't reach your local Community Edition.
- **Keep Git as the source of truth.** Look at `entropy-data datacontracts import-from-git --help`. How would you publish contracts automatically on every merge to `main`?
