Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/release-inventory-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,38 @@ env:
DOCKER_BUILDKIT: "1"

jobs:
downstream-bundle-profiles:
name: Downstream bundle profiles
runs-on: ubuntu-latest
steps:
- name: Check out release profiles
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.14"

- name: Install profile validator dependency
run: python -m pip install --disable-pip-version-check packaging

- name: Validate profiles and checked-in locks
run: python scripts/ci/manage_bundle_profiles.py check

- name: Generate profile inventory artifacts
run: |
python scripts/ci/manage_bundle_profiles.py compile \
--output-dir bundle-profile-inventories

- name: Upload profile inventory artifacts
uses: actions/upload-artifact@v6
with:
name: downstream-bundle-profile-inventories
path: bundle-profile-inventories/*.lock.json
if-no-files-found: error

python-inventory:
name: Python ${{ matrix.python-version }} / ${{ matrix.profile }}
if: ${{ inputs.main-artifact-name != '' }}
Expand Down
110 changes: 110 additions & 0 deletions docs/docs/Deployment/downstream-bundle-profiles.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
title: Downstream bundle profiles
slug: /deployment/downstream-bundle-profiles
---

Downstream Langflow application images must select their provider inventory at
image-build time. A bundle profile records that selection separately from the
Dockerfile so the same reviewed source can be used by local builds, CI, release
workflows, and downstream repositories. Runtime package installation or
registry mutation is not supported.

The release branch stores the profile source in
`scripts/ci/bundle_profiles.json` and its deterministic resolved manifests in
`scripts/ci/bundle_profile_locks/`. The `enterprise-hardened` profile is the
reviewed profile for the supported Enterprise UBI/hardened application image
family. The contract maps the `enterprise-ubi-hardened` image family to that
profile with `install_phase: build`; CI rejects unknown profiles, runtime
installation phases, and profiles that no supported image family selects. It
includes:

- a bounded Langflow application range;
- a bounded LFX bundle API range;
- every bundle distribution, its selected extras, its bounded compatible
range, and the providers it owns;
- the approved package index; and
- ownership and update cadence.

## Select a profile during an image build

Name the profile explicitly in the downstream Dockerfile and install the exact
requirements from its checked-in lock. For example:

```dockerfile
ARG LANGFLOW_BUNDLE_PROFILE=enterprise-hardened

COPY --from=oss-source /langflow-oss/scripts/ci/manage_bundle_profiles.py /tmp/bundle-profile/
COPY --from=oss-source /langflow-oss/scripts/ci/bundle_profile_locks/ /tmp/bundle-profile/locks/

RUN test "$LANGFLOW_BUNDLE_PROFILE" = "enterprise-hardened" \
&& python /tmp/bundle-profile/manage_bundle_profiles.py requirements \
--lock "/tmp/bundle-profile/locks/${LANGFLOW_BUNDLE_PROFILE}.lock.json" \
--bundles-only \
> /tmp/bundle-profile/requirements.txt \
&& uv pip install --requirement /tmp/bundle-profile/requirements.txt
```

This example uses the `oss-source` stage from the Enterprise image build so the
profile and application source always come from the same immutable OSS commit.
The build stage must have the Python `packaging` distribution available to run
the profile utility. `--bundles-only` is appropriate when the application is
already installed from that pinned source; omit it when the lock should also
install the exact Langflow application distribution. Keep the profile argument
fixed in the Dockerfile or the release build configuration so a moving
environment variable cannot change an already-built image.

After installation, verify the environment and write the reviewable component
inventory into the image:

```dockerfile
RUN python /tmp/bundle-profile/manage_bundle_profiles.py verify-installed \
--lock "/tmp/bundle-profile/locks/${LANGFLOW_BUNDLE_PROFILE}.lock.json" \
--output /usr/share/langflow/bundle-profile-inventory.json
```

The verifier fails on missing or undeclared bundle distributions, version
drift, missing or undeclared providers, duplicate providers, and incompatible
Langflow or LFX versions. Verification only inspects the build environment; it
does not install packages or mutate a registry.

## Update a profile

Profile ownership is shared by the Langflow Enterprise and Langflow release
maintainers. Review every profile on each Langflow minor release and whenever a
provider is added, removed, or graduated.

1. Edit `scripts/ci/bundle_profiles.json`. Adding or removing a provider must
change the explicit distribution and provider inventory.
2. Regenerate the resolved manifests:

```bash
python scripts/ci/manage_bundle_profiles.py compile \
--output-dir scripts/ci/bundle_profile_locks
```

3. Validate the source and locks:

```bash
python scripts/ci/manage_bundle_profiles.py check
python -m pytest scripts/ci/test_bundle_profiles.py -q
```

4. Review the profile and lock diffs together. CI rejects unknown bundles,
invalid extras, unbounded or incompatible ranges, duplicate providers,
application dependency drift, and stale locks.
5. Build the downstream image, run `verify-installed`, and retain the emitted
inventory JSON with the image release artifacts.

## Emergency pin or rollback

For a bundle-only incident, narrow the affected bundle's bounded source range,
regenerate the lock, rebuild the image, and keep both changes in the emergency
PR. Do not patch a package into a running pod. To roll back an image, restore
the previously reviewed profile and lock together and rebuild from the same
source revision and indexes. Retain the old image digest and inventory artifact
until the replacement passes the release gate.

If a provider must be removed, delete its explicit profile entry, regenerate
the lock, and verify that the inventory artifact removes the same distribution
and provider. A lock-only edit is rejected because it would bypass profile
review.
5 changes: 5 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ module.exports = {
id: "Deployment/deployment-docker",
label: "Langflow Docker images"
},
{
type: "doc",
id: "Deployment/downstream-bundle-profiles",
label: "Downstream bundle profiles"
},
{
type: "doc",
id: "Deployment/deployment-caddyfile",
Expand Down
226 changes: 226 additions & 0 deletions scripts/ci/bundle_profile_locks/enterprise-hardened.lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
{
"application": {
"distribution": "langflow",
"requested": ">=1.12.0,<1.13.0",
"requirement": "langflow==1.12.0",
"resolved_version": "1.12.0"
},
"bundle_api": {
"distribution": "lfx",
"requested": ">=1.12.0.dev0,<1.13.0",
"requirement": "lfx==1.12.0",
"resolved_version": "1.12.0"
},
"bundles": [
{
"distribution": "lfx-amazon",
"extras": [],
"providers": [
"lfx-amazon"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-amazon==0.1.2",
"resolved_version": "0.1.2"
},
{
"distribution": "lfx-anthropic",
"extras": [],
"providers": [
"lfx-anthropic"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-anthropic==0.1.3",
"resolved_version": "0.1.3"
},
{
"distribution": "lfx-arxiv",
"extras": [],
"providers": [
"lfx-arxiv"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-arxiv==0.1.3",
"resolved_version": "0.1.3"
},
{
"distribution": "lfx-cohere",
"extras": [],
"providers": [
"lfx-cohere"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-cohere==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-datastax",
"extras": [],
"providers": [
"lfx-datastax"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-datastax==0.1.2",
"resolved_version": "0.1.2"
},
{
"distribution": "lfx-docling",
"extras": [],
"providers": [
"lfx-docling"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-docling==0.1.4",
"resolved_version": "0.1.4"
},
{
"distribution": "lfx-duckduckgo",
"extras": [],
"providers": [
"lfx-duckduckgo"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-duckduckgo==0.1.3",
"resolved_version": "0.1.3"
},
{
"distribution": "lfx-empiriolabs",
"extras": [],
"providers": [
"lfx-empiriolabs"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-empiriolabs==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-exa",
"extras": [],
"providers": [
"lfx-exa"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-exa==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-firecrawl",
"extras": [],
"providers": [
"lfx-firecrawl"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-firecrawl==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-ibm",
"extras": [],
"providers": [
"lfx-ibm"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-ibm==0.1.3",
"resolved_version": "0.1.3"
},
{
"distribution": "lfx-nextplaid",
"extras": [],
"providers": [
"lfx-nextplaid"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-nextplaid==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-openai",
"extras": [],
"providers": [
"lfx-openai"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-openai==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-openai-compatible",
"extras": [],
"providers": [
"lfx-openai-compatible"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-openai-compatible==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-oracle",
"extras": [],
"providers": [
"lfx-oracle"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-oracle==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-paddle",
"extras": [],
"providers": [
"lfx-paddle"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-paddle==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-valkey",
"extras": [],
"providers": [
"lfx-valkey"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-valkey==0.1.1",
"resolved_version": "0.1.1"
},
{
"distribution": "lfx-vllm",
"extras": [],
"providers": [
"lfx-vllm"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-vllm==0.1.1",
"resolved_version": "0.1.1"
}
],
"image_families": [
"enterprise-ubi-hardened"
],
"indexes": [
"https://pypi.org/simple"
],
"profile": "enterprise-hardened",
"profile_digest": "sha256:2bf2a71f550faa0c",
"providers": [
"lfx-amazon",
"lfx-anthropic",
"lfx-arxiv",
"lfx-cohere",
"lfx-datastax",
"lfx-docling",
"lfx-duckduckgo",
"lfx-empiriolabs",
"lfx-exa",
"lfx-firecrawl",
"lfx-ibm",
"lfx-nextplaid",
"lfx-openai",
"lfx-openai-compatible",
"lfx-oracle",
"lfx-paddle",
"lfx-valkey",
"lfx-vllm"
],
"python": ">=3.10,<3.15",
"schema_version": 1
}
Loading
Loading