Skip to content
Open
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
58 changes: 48 additions & 10 deletions .github/workflows/deploy-storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,45 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened, closed]

permissions:
contents: write
pull-requests: write

jobs:
build-and-deploy:
runs-on: ubuntu-latest
# PR previews publish unreviewed HTML to the live Pages origin, so they
# are limited to same-repo branches (fork PRs also have a read-only
# GITHUB_TOKEN and could never deploy — this skips the red-noise run).
if: >-
github.event.action != 'closed' &&
(github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository)
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.4.0
with:
version: 9.7.0

- name: Authenticate npm registry
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.TEST_NPM_TOKEN }}" > ~/.npmrc
# No npm auth: all dependencies are public.

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
Expand All @@ -51,7 +62,7 @@ jobs:
# Get the GitHub Pages URL dynamically
- name: Get Pages URL
id: pages
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const { data } = await github.rest.repos.getPages({
Expand All @@ -63,7 +74,7 @@ jobs:
# Main branch → deploy to root
- name: Deploy (main)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./packages/react/storybook-static
Expand All @@ -76,7 +87,7 @@ jobs:
# PR → deploy to /pr/<number>/ subdirectory (doesn't touch main)
- name: Deploy (PR preview)
if: github.event_name == 'pull_request'
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./packages/react/storybook-static
Expand All @@ -89,7 +100,7 @@ jobs:

- name: Add Storybook link to PR description
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const url = `${{ steps.pages.outputs.url }}pr/${{ github.event.pull_request.number }}/`;
Expand All @@ -114,3 +125,30 @@ jobs:
pull_number: context.issue.number,
body: newBody,
});

# Remove the pr/<number>/ preview from gh-pages when the PR closes so the
# live origin doesn't accumulate stale unreviewed HTML.
cleanup-preview:
runs-on: ubuntu-latest
if: >-
github.event_name == 'pull_request' &&
github.event.action == 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Checkout gh-pages
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: gh-pages

- name: Remove preview directory
run: |
PR_DIR="pr/${{ github.event.pull_request.number }}"
if [ -d "$PR_DIR" ]; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git rm -r --quiet "$PR_DIR"
git commit -m "chore: remove Storybook preview for closed PR #${{ github.event.pull_request.number }}"
git push origin gh-pages
else
echo "No preview directory $PR_DIR — nothing to clean up"
fi
76 changes: 27 additions & 49 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Release & Publish
#
# Gated publish flow:
# 1. PR merged to main → triggers build & test
# 1. PR merged to main → the FULL quality-gate suite runs (reusable
# test.yml: typecheck, bundle budget, tests, CSP, browser E2E,
# Storybook smoke/visual, Next.js integration)
# 2. A reviewer approves the publish in the Actions UI (environment gate)
# 3. Package is published to npm via trusted publishing (OIDC)
# 3. Package is published to npm via trusted publishing (OIDC) with
# provenance, from the exact SHA the gates ran against
#
# Version: reads the latest published version from npm and bumps from that.
# Defaults to patch. Add a `release:minor` or `release:major` PR label to
Expand Down Expand Up @@ -36,48 +39,18 @@ on:
default: patch

jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.7.0

- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build packages
run: pnpm build

- name: Run tests
run: pnpm test
# The FULL quality-gate suite (typecheck, bundle budget, unit tests, CSP
# gate, browser E2E, Storybook smoke + visual drift, Next.js integration)
# is a hard dependency of publishing — a publish cannot be approved while
# any advertised gate is red.
gates:
name: Quality gates
uses: ./.github/workflows/test.yml
secrets: inherit

publish:
name: Publish to npm
needs: build-and-test
needs: gates
runs-on: ubuntu-latest
concurrency:
group: publish
Expand All @@ -89,33 +62,37 @@ jobs:
pull-requests: read

steps:
# Check out the SHA the gates ran against — main may have advanced
# between the test run and the environment approval.
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
ref: ${{ github.sha }}

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.4.0
with:
version: 9.7.0

# setup-node with registry-url enables OIDC token exchange for npm publish.
# All @unlayer deps are public — no NPM_TOKEN needed.
- name: Setup Node.js 20 (with npm registry for OIDC)
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
registry-url: https://registry.npmjs.org

# Pinned major — the publish path must not drift with whatever npm
# ships the day of a release.
- name: Update npm for trusted publishing
run: npm install -g npm@latest
run: npm install -g npm@11

- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
Expand Down Expand Up @@ -185,13 +162,14 @@ jobs:
# Two-step publish: pnpm pack resolves catalog: protocol to real
# version numbers, then npm publish handles OIDC trusted publishing.
# npm publish alone does NOT understand catalog: and would ship broken deps.
# No --provenance flag — not supported for private source repos.
# --provenance: the repo is public, so consumers get a verifiable
# supply-chain attestation linking the tarball to this workflow run.
- name: Publish to npm (trusted publishing)
working-directory: packages/react
run: |
pnpm pack --pack-destination /tmp
TARBALL=$(ls /tmp/unlayer-react-elements-*.tgz)
npm publish "$TARBALL" --access public
npm publish "$TARBALL" --access public --provenance

- name: Tag release
run: |
Expand Down
40 changes: 23 additions & 17 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
name: Test & Coverage

# Also callable as a reusable workflow (workflow_call) so publish.yml can make
# the FULL gate suite a hard dependency of publishing — not a parallel,
# non-blocking run.
on:
push:
branches: [ main, develop ]
branches: [ main ]
pull_request:
branches: [ main, develop ]
branches: [ main ]
workflow_call:

permissions:
contents: read

jobs:
test:
name: Run Tests & Coverage
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Setup Node.js 20
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.4.0
with:
version: 9.7.0

- name: Authenticate npm registry
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.TEST_NPM_TOKEN }}" > ~/.npmrc
# No npm auth: all dependencies are public.

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
Expand All @@ -55,16 +61,16 @@ jobs:
run: pnpm --filter @unlayer/react-elements typecheck

# ── Bundle size budget ──────────────────────────────────────
# Fail if the react-elements ESM bundle exceeds 75KB.
# Current size: ~70KB (14+ components + image width-pinning geometry
# + per-mode full-document shells for renderToHtml).
# Fail if the react-elements ESM bundle exceeds 100KB.
# Current size: ~82KB (14+ components + image width-pinning geometry,
# per-mode full-document shells, escaping + validation hardening).
# The budget tracks the unminified ESM output as a proxy for code volume;
# it still flags accidental dependency bundling (any real dep is 10KB+).
- name: Check bundle size
run: |
BUNDLE="packages/react/dist/index.js"
SIZE=$(wc -c < "$BUNDLE" | tr -d ' ')
MAX_SIZE=75000
MAX_SIZE=100000
echo "Bundle: $BUNDLE"
echo "Size: $SIZE bytes (budget: $MAX_SIZE bytes)"
if [ "$SIZE" -gt "$MAX_SIZE" ]; then
Expand Down Expand Up @@ -134,14 +140,14 @@ jobs:
npm run build

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true

- name: Upload coverage artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: coverage-reports
path: |
Expand Down
Loading
Loading