Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ on:
push:
branches: [main]
tags:
- 'v*'
- 'core-v*'
- 'browser-v*'
- 'node-server-v*'
pull_request:
release:
types: [created]
Expand Down
132 changes: 98 additions & 34 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,54 @@ jobs:
validate-release:
runs-on: ubuntu-latest
environment: production
outputs:
package-dir: ${{ steps.parse-tag.outputs.package-dir }}
package-version: ${{ steps.parse-tag.outputs.package-version }}
package-prefix: ${{ steps.parse-tag.outputs.package-prefix }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Parse release tag
id: parse-tag
run: |
TAG="${{ github.event.release.tag_name }}"
echo "Release tag: $TAG"

# Map tag prefix to package directory
if [[ "$TAG" =~ ^core-v(.+)$ ]]; then
PACKAGE_DIR="packages/core"
PACKAGE_VERSION="${BASH_REMATCH[1]}"
PACKAGE_PREFIX="core"
elif [[ "$TAG" =~ ^browser-v(.+)$ ]]; then
PACKAGE_DIR="packages/browser"
PACKAGE_VERSION="${BASH_REMATCH[1]}"
PACKAGE_PREFIX="browser"
elif [[ "$TAG" =~ ^node-server-v(.+)$ ]]; then
PACKAGE_DIR="packages/node-server"
PACKAGE_VERSION="${BASH_REMATCH[1]}"
PACKAGE_PREFIX="node-server"
else
echo "❌ Unrecognized tag format: $TAG"
echo "Expected: core-v{version}, browser-v{version}, or node-server-v{version}"
exit 1
fi

echo "package-dir=$PACKAGE_DIR" >> "$GITHUB_OUTPUT"
echo "package-version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"
echo "package-prefix=$PACKAGE_PREFIX" >> "$GITHUB_OUTPUT"

- name: Validate release version
run: |
# Check if the tag matches the version in lerna.json
RELEASE_VERSION="${{ github.event.release.tag_name }}"
LERNA_VERSION=$(node -p "require('./lerna.json').version")
PACKAGE_DIR="${{ steps.parse-tag.outputs.package-dir }}"
EXPECTED_VERSION="${{ steps.parse-tag.outputs.package-version }}"
ACTUAL_VERSION=$(node -p "require('./$PACKAGE_DIR/package.json').version")

if [[ "v$LERNA_VERSION" != "$RELEASE_VERSION" ]]; then
echo "❌ Release tag $RELEASE_VERSION doesn't match lerna.json version v$LERNA_VERSION"
if [[ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]]; then
echo "❌ Release tag version $EXPECTED_VERSION doesn't match $PACKAGE_DIR/package.json version $ACTUAL_VERSION"
exit 1
fi

echo "✅ Release validation passed"
echo "✅ Release validation passed: $PACKAGE_DIR version $ACTUAL_VERSION"

build-and-publish:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -74,60 +107,91 @@ jobs:
echo "Using 'alpha' npm tag for prerelease $RELEASE_TAG"
fi

- name: Publish core package
- name: Publish core package first (if needed)
if: needs.validate-release.outputs.package-prefix != 'core'
run: |
PACKAGE_DIR="${{ needs.validate-release.outputs.package-dir }}"
PACKAGE_PREFIX="${{ needs.validate-release.outputs.package-prefix }}"

# Check if the tagged package depends on core
CORE_DEP_VERSION=$(node -p "
try {
const pkg = require('./$PACKAGE_DIR/package.json');
(pkg.dependencies || {})['@datadog/flagging-core'] || ''
} catch { '' }
")

if [ -z "$CORE_DEP_VERSION" ]; then
echo "Package $PACKAGE_PREFIX does not depend on @datadog/flagging-core, skipping core publish"
exit 0
fi

CORE_ACTUAL_VERSION=$(node -p "require('./packages/core/package.json').version")

# Check if the required version of core is already on npm
echo "Checking if @datadog/flagging-core@$CORE_ACTUAL_VERSION is already on npm..."
if npm view "@datadog/flagging-core@$CORE_ACTUAL_VERSION" --json > /dev/null 2>&1; then
echo "✅ @datadog/flagging-core@$CORE_ACTUAL_VERSION is already available on npm"
exit 0
fi

echo "⚠️ @datadog/flagging-core@$CORE_ACTUAL_VERSION not found on npm."
echo "Publishing core first..."
echo "//registry.npmjs.org/:_authToken=${{ secrets.ENV_NPM_TOKEN }}" > ~/.npmrc
npm config set access public
cd packages/core
npm publish --tag ${{ steps.npm-tag.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.ENV_NPM_TOKEN }}

- name: Wait for core package to be available
- name: Wait for core package to be available (if needed)
if: needs.validate-release.outputs.package-prefix != 'core'
run: |
# Get the version from lerna.json
VERSION=$(node -p "require('./lerna.json').version")
PACKAGE_NAME="@datadog/flagging-core"
PACKAGE_DIR="${{ needs.validate-release.outputs.package-dir }}"

# Check if the tagged package depends on core
CORE_DEP_VERSION=$(node -p "
try {
const pkg = require('./$PACKAGE_DIR/package.json');
(pkg.dependencies || {})['@datadog/flagging-core'] || ''
} catch { '' }
")

if [ -z "$CORE_DEP_VERSION" ]; then
echo "Package does not depend on @datadog/flagging-core, skipping wait"
exit 0
fi

CORE_ACTUAL_VERSION=$(node -p "require('./packages/core/package.json').version")

echo "Waiting for $PACKAGE_NAME@$VERSION to be available on npm..."
echo "This may take a few minutes due to npm registry propagation..."
echo "Waiting for @datadog/flagging-core@$CORE_ACTUAL_VERSION to be available on npm..."

# Wait up to 5 minutes (300 seconds) with 10-second intervals
for i in {1..30}; do
echo "Attempt $i/30: Checking if $PACKAGE_NAME@$VERSION is available..."

# Try to fetch the package info from npm
if npm view "$PACKAGE_NAME@$VERSION" --json > /dev/null 2>&1; then
echo "✅ $PACKAGE_NAME@$VERSION is now available on npm!"
echo "Package details:"
npm view "$PACKAGE_NAME@$VERSION" --json
break
echo "Attempt $i/30: Checking if @datadog/flagging-core@$CORE_ACTUAL_VERSION is available..."

if npm view "@datadog/flagging-core@$CORE_ACTUAL_VERSION" --json > /dev/null 2>&1; then
echo "✅ @datadog/flagging-core@$CORE_ACTUAL_VERSION is now available on npm!"
exit 0
fi

if [ $i -eq 30 ]; then
echo "❌ Timeout: $PACKAGE_NAME@$VERSION is still not available after 5 minutes"
echo "This might indicate an issue with the npm publish or registry propagation"
echo "❌ Timeout: @datadog/flagging-core@$CORE_ACTUAL_VERSION is still not available after 5 minutes"
exit 1
fi

echo "Package not yet available, waiting 10 seconds..."
sleep 10
done

- name: Publish browser package
- name: Publish tagged package
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.ENV_NPM_TOKEN }}" > ~/.npmrc
npm config set access public
cd packages/browser
npm publish --tag ${{ steps.npm-tag.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.ENV_NPM_TOKEN }}
PACKAGE_DIR="${{ needs.validate-release.outputs.package-dir }}"
PACKAGE_PREFIX="${{ needs.validate-release.outputs.package-prefix }}"

- name: Publish node-server package
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.ENV_NPM_TOKEN }}" > ~/.npmrc
npm config set access public
cd packages/node-server
cd "$PACKAGE_DIR"
echo "Publishing $(node -p "require('./package.json').name")..."
npm publish --tag ${{ steps.npm-tag.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.ENV_NPM_TOKEN }}
17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# openfeature-js-client

Monorepo using **Lerna** with **fixed versioning** — all packages share the version in `lerna.json`.
Monorepo using **Lerna** with **independent versioning** — each package has its own version in its `package.json`.

## Release

Expand All @@ -9,8 +9,19 @@ See [CONTRIBUTING.md](CONTRIBUTING.md#creating-a-release) for the full release p
Key commands:

```bash
# Non-interactive version bump (use this instead of interactive `yarn release`)
yarn lerna version <VERSION> --exact --force-publish --yes
# Interactive version bump (prompts per changed package)
yarn release

# Non-interactive version bump for a specific package
yarn lerna version <VERSION> --exact --yes --scope=@datadog/openfeature-browser
```

### Tag naming convention

```
core-v{version} → publishes @datadog/flagging-core
browser-v{version} → publishes @datadog/openfeature-browser
node-server-v{version} → publishes @datadog/openfeature-node-server
```

## Packages
Expand Down
76 changes: 38 additions & 38 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ This is a monorepo managed with Lerna that contains multiple packages:

- **`@datadog/flagging-core`** - Runtime-agnostic flag-evaluation logic
- **`@datadog/openfeature-browser`** - Browser-specific bindings for OpenFeature
- **`@datadog/openfeature-node-server`** - Node.js server bindings for OpenFeature

The project uses **fixed versioning**, meaning all packages share the same version number and are released together. The version is managed centrally in `lerna.json`.
The project uses **independent versioning**, meaning each package has its own version number in its `package.json` and can be released independently.

## Development Setup

Expand Down Expand Up @@ -92,7 +93,7 @@ All packages are published with the `latest` npm tag.

1. **Switch to a feature branch:**
```bash
git checkout -b release/v1.2.3
git checkout -b release/browser-v1.2.0
```

#### Step 2: Prepare Package Dependencies
Expand All @@ -105,44 +106,46 @@ All packages are published with the `latest` npm tag.

This command:
- Validates you're not on the `main` branch
- Runs `lerna version --exact --force-publish` to update the version
- Prompts for the new version number (applied to all packages)
- Creates version commits and tags
- Updates all package versions to match
- Pushes version tag to Github
- Runs `lerna version --exact` to update versions
- Prompts for a version bump for each changed package individually
- Creates version commits and per-package git tags
- Pushes version tags to Github

#### Step 3: Publish via GitHub Release

**Publishing is fully automated via GitHub workflows!**

Each package uses its own tag naming convention:

- `core-v{version}` → publishes `@datadog/flagging-core`
- `browser-v{version}` → publishes `@datadog/openfeature-browser`
- `node-server-v{version}` → publishes `@datadog/openfeature-node-server`

1. **Create a GitHub Release:**
- Go to the GitHub repository
- Click "Releases" → "Create a new release"
- Set the tag to match your version (e.g., `v1.1.0`)
- Set the tag to match the package you want to publish (e.g., `browser-v1.2.0`)
- Add release notes describing your changes or use the `Generate Release Notes` button
- Click "Publish release"
- Repeat for each package that needs publishing

2. **Automated Publishing Workflow:**

The `release.yaml` workflow will automatically trigger and:

**Validation Phase:**
- Checks that the GitHub release tag matches the version in `lerna.json`
- Parses the tag to determine which package to publish (e.g., `browser-v1.2.0` → `packages/browser`)
- Validates the tag version matches that package's `package.json` version
- Fails fast if validation doesn't pass

**Build and Publish Phase:**
- Installs dependencies with `yarn install --immutable`
- Builds all packages in release mode (`BUILD_MODE=release`)
- Creates package tarballs with `yarn lerna run pack --stream`

**Publishing Sequence:**
1. **Publishes core package first** (`@datadog/flagging-core`)
2. **Waits for npm registry propagation**
- Polls npm registry for up to 5 minutes
- Ensures core package is available before proceeding
- Prevents dependency resolution issues
3. **Publishes browser package** (`@datadog/openfeature-browser`)
4. **Publishes node-server package** (`@datadog/openfeature-node-server`)
**Publishing:**
- If the tagged package depends on `@datadog/flagging-core` and core is not yet on npm at the required version, publishes core first and waits for registry propagation
- Publishes the tagged package to npm

### Package-Specific Build Commands

Expand Down Expand Up @@ -196,30 +199,29 @@ yarn pack

### Version Management

Since this project uses **fixed versioning**:
This project uses **independent versioning**:

- All packages share the same version number (managed in `lerna.json`)
- When running `yarn release`, Lerna will prompt for a single version update
- All package versions are automatically synchronized
- Peer dependencies are automatically updated to match the fixed version
- A single version commit and tag is created for the entire project
- Each package has its own version in its `package.json`
- When running `yarn release`, Lerna prompts for a version bump per changed package
- Internal dependencies (e.g. `@datadog/flagging-core`) are updated to match actual versions
- Per-package git tags and version commits are created (e.g., `core-v1.1.0`, `browser-v1.2.0`)
- Packages can be released independently — only the tagged package is published

### Automated Release Workflow Details

The GitHub Actions workflow (`release.yaml`) includes several safety measures:

1. **Version Consistency Check:**
- Compares GitHub release tag with `lerna.json` version
- Ensures tags and versions are synchronized
1. **Tag Parsing and Validation:**
- Parses the per-package tag (e.g. `browser-v1.2.0` → `packages/browser`, version `1.2.0`)
- Validates the tag version matches the package's `package.json`

2. **Dependency Coordination:**
- Core package is published first
- Waits for npm registry propagation (up to 5 minutes)
- Browser package gets updated core dependency automatically
- If the tagged package depends on `@datadog/flagging-core`, checks if the required version is on npm
- If not available, publishes core first and waits for registry propagation (up to 5 minutes)

3. **Build Integrity:**
- Uses `BUILD_MODE=release` for production builds
- Replaces build environment variables correctly
- Replaces build environment variables correctly per-package
- Creates both npm packages and CDN bundles

### Testing Before Release
Expand Down Expand Up @@ -264,23 +266,21 @@ The GitHub Actions workflow (`release.yaml`) includes several safety measures:
- Solution: Create a feature branch for releases

2. **Version mismatch in GitHub workflow:**
- Error: "Release tag doesn't match lerna.json version"
- Solution: Ensure the GitHub release tag exactly matches `v{version}` format where `{version}` is from `lerna.json`
- Error: "Release tag version doesn't match package.json version"
- Solution: Ensure the GitHub release tag matches the format `{package}-v{version}` where `{version}` matches the package's `package.json` (e.g., `browser-v1.2.0`)

3. **Build environment issues:**
- Ensure `BUILD_MODE` and `SDK_SETUP` are set correctly
- Check that all dependencies are installed

4. **Version synchronization issues:**
- Run `yarn version` to update peer dependencies
- Check that all package versions match the version in `lerna.json`
- Run `yarn version` to update internal dependency versions
- Check that internal dependency versions in each package match the actual version in the dependency's `package.json`

5. **GitHub workflow failures:**
- Check the Actions tab for detailed error logs
- Ensure GitHub secrets are properly configured:
- `NPM_PUBLISH_TOKEN_FLAGGING_CORE` for core package
- `NPM_PUBLISH_TOKEN` for browser package
- Verify the release tag matches the version in `lerna.json`
- Ensure GitHub secrets are properly configured
- Verify the release tag uses the correct format: `core-v{version}`, `browser-v{version}`, or `node-server-v{version}`

6. **npm registry propagation delays:**
- The workflow waits up to 5 minutes for the core package to be available
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"npmClient": "yarn",
"version": "1.1.0"
"version": "independent"
}
5 changes: 2 additions & 3 deletions scripts/cli
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ cmd_lint () {

cmd_release () {
[[ `git branch --show-current` != "main" ]] || fail 'please do not release from `main` branch'
# We should publish all packages regardless of if there are changes in each.
# --force-publish will skip the `lerna changed` check for changed packages
yarn lerna version --exact --force-publish
# Independent versioning: Lerna will prompt for each changed package individually
yarn lerna version --exact
}

cmd_version () {
Expand Down
Loading