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
2 changes: 2 additions & 0 deletions .changeset/cdn-css-upload-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
88 changes: 88 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ on:
push:
branches:
- main
# Manual recovery path: if the CDN stylesheet upload fails after packages were
# already published to npm, a job re-run cannot retry it (changesets reports
# nothing newly published on the re-run). Instead, run this workflow manually
# from main: the changesets step is skipped and the CDN upload always runs,
# built from the tag of the latest published UI release so the uploaded CSS
# matches what is on npm. The upload is idempotent.
# Dispatches from any ref other than main are no-ops (job-level guard below),
# so branch CSS can never overwrite the production stylesheet.
workflow_dispatch:
Comment thread
greptile-apps[bot] marked this conversation as resolved.

concurrency: ${{ github.workflow }}-${{ github.ref }}

Expand All @@ -16,6 +25,10 @@ permissions:
jobs:
release:
name: Release
# Only ever run against main. Pushes are already filtered by the trigger;
# this guards workflow_dispatch, which GitHub allows from any ref that
# contains the workflow file.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -24,6 +37,26 @@ jobs:
# This is required for changesets to access commit history
fetch-depth: 0

# On manual recovery runs, build the exact source of the latest published
# UI package instead of current HEAD: main may have advanced past the
# release commit, and the uploaded stylesheet must always match what is
# on npm. The npm "latest" dist-tag is the source of truth for which
# version that is (git tag dates can lie: recreated tags or prereleases
# can carry the newest creation date); changesets pushes a matching
# git tag per published version.
- name: Check out latest published UI release (manual runs)
if: github.event_name == 'workflow_dispatch'
run: |
git fetch --tags --quiet origin
version="$(npm view @youversion/platform-react-ui version)"
tag="@youversion/platform-react-ui@${version}"
if ! git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
echo "::error::npm latest is ${version} but tag ${tag} does not exist in this repository; cannot determine which stylesheet to publish."
exit 1
fi
echo "Building stylesheet from ${tag} (npm latest)"
git checkout --detach "refs/tags/${tag}"

- name: Setup pnpm
uses: pnpm/action-setup@v4

Expand All @@ -45,6 +78,9 @@ jobs:
run: pnpm build

- name: Create Release Pull Request or Publish to NPM
# Skipped on workflow_dispatch (manual CDN re-upload) so a manual run can
# never version or publish packages as a side effect.
if: github.event_name == 'push'
id: changesets
uses: changesets/action@v1
with:
Expand All @@ -67,3 +103,55 @@ jobs:
echo "The following packages were published to NPM:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[] | "- \(.name)@\(.version)"' >> $GITHUB_STEP_SUMMARY

Comment thread
greptile-apps[bot] marked this conversation as resolved.
# When a new @youversion/platform-react-ui version is published, upload its
# compiled stylesheet to the cdn.youversion.com origin bucket so it is served
# at https://cdn.youversion.com/platform/<major>/bible.css (YPE-1733).
# The <major> path segment comes from packages/ui/CDN_CSS_MAJOR_VERSION and is
# bumped only for breaking CSS changes — see PUBLISHING.md.
# Also runs on workflow_dispatch as the manual recovery path (see `on:` above).
- name: Authenticate to Google Cloud
if: github.event_name == 'workflow_dispatch' || (steps.changesets.outputs.published == 'true' && contains(fromJSON(steps.changesets.outputs.publishedPackages).*.name, '@youversion/platform-react-ui'))
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}

- name: Set up Cloud SDK
if: github.event_name == 'workflow_dispatch' || (steps.changesets.outputs.published == 'true' && contains(fromJSON(steps.changesets.outputs.publishedPackages).*.name, '@youversion/platform-react-ui'))
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2

- name: Publish UI stylesheet to CDN
if: github.event_name == 'workflow_dispatch' || (steps.changesets.outputs.published == 'true' && contains(fromJSON(steps.changesets.outputs.publishedPackages).*.name, '@youversion/platform-react-ui'))
Comment thread
greptile-apps[bot] marked this conversation as resolved.
run: |
# Feature-flag gate (feature.platform.sdkCssCdn, defined in the
# transformers flag manifest): read the prod Firebase Remote Config
# *server* template and only upload when the flag's value is "true".
# A missing parameter or "false" means CDN publishing is disabled
# (ship dark) — flip the value in the prod Firebase console to
# launch or kill without a code change. This simple check reads
# defaultValue only; conditional values / percentage rollouts are
# not evaluated. A template fetch failure fails this step (visibly)
# rather than guessing.
TOKEN="$(gcloud auth print-access-token)"
FLAG_VALUE="$(curl -sS --fail-with-body \
-H "Authorization: Bearer $TOKEN" \
"https://firebaseremoteconfig.googleapis.com/v1/projects/yvplatform-prod/namespaces/firebase-server/remoteConfig" \
| jq -r '.parameters.feature_platform_sdkCssCdn.defaultValue.value // "false"')"
if [ "$FLAG_VALUE" != "true" ]; then
echo "::notice::CDN stylesheet publishing is disabled (feature.platform.sdkCssCdn=${FLAG_VALUE} in prod Remote Config); skipping upload."
echo "## CDN Stylesheet :art:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Skipped: \`feature.platform.sdkCssCdn\` is \`${FLAG_VALUE}\` in the prod Remote Config server template." >> $GITHUB_STEP_SUMMARY
exit 0
fi

CSS_MAJOR_VERSION="$(tr -d '[:space:]' < packages/ui/CDN_CSS_MAJOR_VERSION)"
test -s packages/ui/dist/tailwind.css
gcloud storage cp packages/ui/dist/tailwind.css \
"gs://cdn-yv-platform-prod/platform/${CSS_MAJOR_VERSION}/bible.css" \
--cache-control="public, max-age=300, must-revalidate" \
--content-type="text/css"
echo "## CDN Stylesheet :art:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Uploaded \`packages/ui/dist/tailwind.css\` to https://cdn.youversion.com/platform/${CSS_MAJOR_VERSION}/bible.css" >> $GITHUB_STEP_SUMMARY
50 changes: 50 additions & 0 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,56 @@ Required packages:

The workflow keeps `NPM_TOKEN` as a fallback for any package where Trusted Publishing isn't configured yet. If you set one, generate it as an **Automation token** — not a Publish or personal-user token. Automation tokens explicitly bypass npm's 2FA-on-publish, which CI cannot satisfy. A Publish token will fail every publish with `EOTP` / "need a one-time password" (see [`RELEASE-RUNBOOK.md` §5](./RELEASE-RUNBOOK.md#5-otp--2fa-error-class-wrong-token-type)). Remove `NPM_TOKEN` once all three packages are on Trusted Publishing.

## CDN Stylesheet (bible.css)

When the Release workflow publishes a new `@youversion/platform-react-ui` version, it also uploads the package's compiled stylesheet (`packages/ui/dist/tailwind.css`) to the YouVersion static-asset CDN, where it is served at:

```
https://cdn.youversion.com/platform/<major>/bible.css
```

This lets non-bundler consumers (e.g. server-rendered pages) link a stable stylesheet URL instead of extracting CSS from the npm package.

### The CSS major version constant

The `<major>` path segment is defined in **one place**: the `packages/ui/CDN_CSS_MAJOR_VERSION` file (currently `1`).

- **Do not bump it for routine releases.** The file at `/platform/<major>/bible.css` is overwritten in place with each UI package release.
- **Bump it only when the CSS changes in a breaking way** (selectors/variables/class names that existing consumers depend on are removed or behave differently). Bumping starts publishing to a new `/platform/<major+1>/bible.css` URL and leaves the old file untouched for existing consumers.

### Feature flag: `feature.platform.sdkCssCdn`

The upload is gated by the `feature.platform.sdkCssCdn` feature flag, defined (like all YouVersion Platform flags) in the transformers repo's `src/flags.yaml` manifest and mirrored to Firebase Remote Config. The workflow reads the **prod server template** (project `yvplatform-prod`, namespace `firebase-server`, parameter key `feature_platform_sdkCssCdn` — Remote Config keys replace dots with underscores).

- **Ship-dark default:** the flag is `false` in prod, so merging the workflow does not start publishing to the CDN by itself.
- **To launch (or kill) CDN publishing:** flip `feature_platform_sdkCssCdn` in the `yvplatform-prod` Firebase console's server template — no code change or deploy needed. A missing parameter evaluates as disabled.
- When the flag is off, the release still publishes to npm normally; the CDN step logs a notice and skips the upload.
- The check reads the parameter's default value only; conditional values / percentage rollouts are not evaluated.

### How the upload authenticates

The workflow uses Workload Identity Federation (OIDC) — no static GCP keys, matching how this repo publishes to npm via trusted publishing:

1. `google-github-actions/auth` exchanges the GitHub Actions OIDC token through the `github-actions-pool` Workload Identity Pool in the `yvplatform-prod` GCP project.
2. Only this repository (`youversion/platform-sdk-react`) may impersonate the dedicated service account `platform-sdk-cdn-publisher@yvplatform-prod.iam.gserviceaccount.com`.
3. That service account can only write objects under the `platform/` prefix of the `cdn-yv-platform-prod` origin bucket (IAM condition), plus read Firebase Remote Config (`roles/cloudconfig.viewer` on `yvplatform-prod`) for the feature-flag check — nothing else.

Repository Actions secrets:

- `WIF_PROVIDER` — full resource name of the Workload Identity Pool provider
- `WIF_SERVICE_ACCOUNT` — `platform-sdk-cdn-publisher@yvplatform-prod.iam.gserviceaccount.com`

### Caching

`bible.css` is a mutable object at a stable URL, so it is uploaded with `Cache-Control: public, max-age=300, must-revalidate`. Consumers pick up new releases within ~5 minutes.

### Troubleshooting the CDN upload

- On `main` pushes, the upload steps only run when `@youversion/platform-react-ui` is among the published packages.
- npm publishing happens before the CDN upload; a failed upload does not affect the npm release.
- **Do not retry by re-running the failed job**: on a re-run, changesets reports nothing newly published, so the CDN steps are skipped. Instead, after fixing the issue, trigger the **Release workflow manually** (Actions → Release → "Run workflow" on `main`). Manual runs skip versioning/publishing entirely; they resolve the npm `latest` dist-tag of `@youversion/platform-react-ui`, check out the matching release tag, rebuild it, and upload its stylesheet — so the CDN always matches what's on npm even if `main` has advanced past the release commit. The upload is idempotent, so this is always safe.
- Manual dispatches from any ref other than `main` are no-ops (job-level guard), so branch CSS can never overwrite the production stylesheet.

## Troubleshooting

### "Version Packages" PR Not Created
Expand Down
1 change: 1 addition & 0 deletions packages/ui/CDN_CSS_MAJOR_VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Loading