diff --git a/.github/workflows/check-release-notes.yml b/.github/workflows/check-release-notes.yml new file mode 100644 index 000000000..b33be3073 --- /dev/null +++ b/.github/workflows/check-release-notes.yml @@ -0,0 +1,31 @@ +name: Check Release Notes + +on: + pull_request: + branches: + - master + paths: + - "docs/release-notes/**" + +jobs: + review-markers: + name: Ensure changelog entries are confirmed + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Enable Corepack (Yarn Berry) + run: corepack enable + + - name: Install dependencies + run: yarn install --immutable + + - name: Check for unresolved review markers + run: yarn check:changelog-review diff --git a/AGENTS.md b/AGENTS.md index 7dc4ccc1e..f7f46895c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,10 +22,15 @@ When editing a generated `changelog.mdx` file, always keep the sibling `changelo The `generate-changelog.ts` script reads `## Skipped PRs` to avoid re-adding manually removed entries on the next run. If `changelog.ai.txt` is not updated, removed PRs will reappear. +PRs for a release are discovered from the release commits (the `(#NNNN)` references between the previous release tag and this version's tag/branch), **not** from a GitHub milestone — so a forgotten milestone no longer drops entries. Run `yarn generate:changelog --version --dry-run` to preview the PRs that would be included. + +**Review markers:** every generated entry gets an MDX comment beneath its `###` heading naming the PR author(s), e.g. `{/* REVIEW-PENDING @author — confirm this entry, then delete this line */}`. Each author must inspect their entry and delete that line. CI (`.github/workflows/check-release-notes.yml`, via `yarn check:changelog-review`) fails on any PR to master that still contains a `REVIEW-PENDING` marker, so release notes cannot be published until every entry is confirmed. + ### Validation and Quality - **MDX/.ai.txt Pairing**: Every `.mdx` file must have a corresponding `.ai.txt` companion file - Run `yarn validate:mdx` to verify pairing before commits/PRs +- Run `yarn check:changelog-review` to verify no unresolved `REVIEW-PENDING` markers remain - Exceptions are defined in `.mdx-validation.json` (supports exact paths and glob patterns) - The validation script (`scripts/validate-mdx-pairing.ts`) checks bidirectionally diff --git a/docs/developer-docs/6.x/navigation.tsx b/docs/developer-docs/6.x/navigation.tsx index 1c3b3f37b..552afa36e 100644 --- a/docs/developer-docs/6.x/navigation.tsx +++ b/docs/developer-docs/6.x/navigation.tsx @@ -161,6 +161,7 @@ export const Navigation = ({ children }: { children: React.ReactNode }) => { + diff --git a/docs/developer-docs/6.x/security/cognito-federation.ai.txt b/docs/developer-docs/6.x/security/cognito-federation.ai.txt new file mode 100644 index 000000000..23ed6df41 --- /dev/null +++ b/docs/developer-docs/6.x/security/cognito-federation.ai.txt @@ -0,0 +1,45 @@ +AI Context: Cognito Federation (cognito-federation.mdx) + +Source of Information: +1. packages/cognito/src/Cognito.tsx — defineExtension with federation prop, Pulumi hook, Admin.BuildParam +2. packages/cognito/src/admin/presentation/Cognito/CognitoSignInConfig.ts — ICognitoSignInConfig interface, FederatedProvider union type +3. packages/cognito/src/admin/presentation/Cognito/CognitoPresenter.ts — resolves CognitoSignInConfig, configures Amplify OAuth, populates vm.signIn +4. packages/cognito/src/admin/DefaultCognitoSignInConfig.ts — auto-generated config from BuildParam +5. packages/cognito/src/api/features/CognitoIdp/CognitoIdentityProvider.ts — detects identities claim, sets external: true, merges custom getIdentity +6. packages/cognito/src/api/features/CognitoIdp/abstractions.ts — CognitoIdpConfig interface +7. packages/api-core/src/features/users/ExternalIdpUserSync/ExternalIdpUserSyncHandler.ts — auto-creates/updates external users on AfterLogin +8. packages/cognito/src/infra/CognitoFederationPulumi.ts — CorePulumi hook calling configureAdminCognitoFederation +9. packages/project-aws/src/pulumi/apps/core/cognitoIdentityProviders/configure.ts — configureAdminCognitoFederation, creates domain/IdP/OAuth client +10. packages/cognito/src/admin/presentation/Cognito/components/FederatedLogin.tsx — renders provider buttons, calls signInWithRedirect +11. packages/cognito/src/admin/presentation/Cognito/components/SignIn.tsx — conditionally renders credentials form, FederatedLogin, Divider +12. skills/user-skills/cognito-federation/SKILL.md — full reference skill +13. skills/user-skills/configure-entraid/SKILL.md — Entra ID specific skill + +Key Documentation Decisions: +1. No step-by-step setup guide — AI assistants with Webiny MCP can generate the config from a prompt; the docs focus on concepts and capabilities +2. "Setting It Up with AI" section shows an example prompt for MCP-equipped AI assistants +3. Custom identity mapping only shows overriding roles/teams — defaults for id, displayName, profile are auto-populated from standard Cognito claims +4. adminConfig async example demonstrates IP whitelisting — the most common advanced use case +5. Deploy order section is essential because core must be deployed first to create the Cognito domain, which must then be configured in the external IdP before admin can work + +Understanding: +The federation prop on handles three concerns: +1. Infrastructure (Pulumi) — configureAdminCognitoFederation() creates User Pool Domain, IdentityProvider resources, and configures OAuth on the UserPoolClient +2. Admin login screen — federation config is passed as Admin.BuildParam; DefaultCognitoSignInConfig reads it and implements CognitoSignInConfig.getConfig(); CognitoPresenter resolves it, configures Amplify.configure() with loginWith.oauth, and populates vm.signIn with title/description/allowCredentialsLogin/federatedProviders +3. API identity — CognitoIdentityProvider.getIdentity() builds default identity from token claims, then merges custom CognitoIdpConfig result on top; detects federated users via Boolean(token.identities) and sets external: true; ExternalIdpUserSyncHandler fires on AfterLogin and creates/updates users when external is true + +FederatedProvider is a union: { name, label } renders a default Button; { name, component } renders a custom React component receiving { signIn } callback. + +The adminConfig extension replaces the auto-generated DefaultCognitoSignInConfig with a custom implementation. Because getConfig() is async, runtime checks (IP whitelisting, environment detection) work naturally. + +Related Documents: +- docs/developer-docs/6.x/security/roles.mdx — RoleFactory, roles referenced by slug in identity mapping +- docs/developer-docs/6.x/security/teams.mdx — TeamFactory, teams referenced by slug in identity mapping +- docs/developer-docs/6.x/core-concepts/di.mdx — DI pattern used by CognitoIdpConfig and CognitoSignInConfig + +Key Code Locations: +- packages/cognito/src/Cognito.tsx — extension definition with federation schema +- packages/cognito/src/admin/presentation/Cognito/CognitoSignInConfig.ts — ICognitoSignInConfig, FederatedProvider +- packages/cognito/src/admin/presentation/Cognito/CognitoPresenter.ts — presenter integration +- packages/cognito/src/api/features/CognitoIdp/CognitoIdentityProvider.ts — identity resolution +- packages/cognito/src/api/features/CognitoIdp/abstractions.ts — CognitoIdpConfig diff --git a/docs/developer-docs/6.x/security/cognito-federation.mdx b/docs/developer-docs/6.x/security/cognito-federation.mdx new file mode 100644 index 000000000..bb4883c22 --- /dev/null +++ b/docs/developer-docs/6.x/security/cognito-federation.mdx @@ -0,0 +1,255 @@ +--- +id: a7k2m9x1 +title: Cognito Federation +description: Add federated sign-in (Google, Microsoft Entra ID, OIDC) to the Admin app while keeping Cognito as your user pool. +--- + +import { Alert } from "@/components/Alert"; + + + +- what Cognito Federation is and when to use it +- how to add external identity providers to the login screen +- how federated users are synced into Webiny +- how to customize identity mapping and login screen behavior + + + +## Overview + +By default, Webiny uses a Cognito User Pool where admin users are created manually through the Admin app. Cognito Federation lets you connect external identity providers — Google, Microsoft Entra ID, Facebook, Apple, or any OIDC-compatible provider — so users can sign in with their existing corporate or social credentials. + +Unlike switching to Okta or Auth0 (which replace Cognito entirely), federation keeps Cognito as your user pool. The external provider handles authentication; Cognito issues the tokens; Webiny manages the users. + +### What Happens When a Federated User Signs In + +1. The user clicks a provider button (e.g., "Sign in with Microsoft") on the login screen. +2. Cognito redirects to the external identity provider. +3. After authentication, Cognito creates a session with an `idToken` that includes an `identities` claim. +4. Webiny detects the `identities` claim and marks the user as **external**. +5. On first login, the user is automatically created in Webiny with the roles and teams derived from token claims. +6. On subsequent logins, the user profile is updated if claims have changed. + +External users appear in the Admin app's user management with a read-only badge — they cannot be edited or deleted through the UI since they are managed by the external identity provider. + +## Configuring Federation + +Add a `federation` prop to the `` extension in your `webiny.config.tsx`. This single declaration handles: + +- **AWS infrastructure** — creates the Cognito User Pool Domain, Identity Provider resources, and configures OAuth on the User Pool Client +- **Admin login screen** — shows provider buttons and configures Amplify for the OAuth redirect flow + +```tsx webiny.config.tsx +import { Cognito } from "@webiny/cognito"; + + +``` + +### Federation Config Reference + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `domain` | `string` | Yes | — | Cognito User Pool domain prefix | +| `callbackUrls` | `string[]` | Yes | — | OAuth callback URLs (localhost for dev, real domain for prod) | +| `logoutUrls` | `string[]` | No | same as `callbackUrls` | OAuth logout redirect URLs | +| `responseType` | `"code"` or `"token"` | No | `"code"` | OAuth response type | +| `allowCredentialsLogin` | `boolean` | No | `true` | Whether to show the email/password form alongside provider buttons | +| `identityProviders` | array | Yes | — | List of external identity providers | + +### Identity Provider Config + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | `"google"`, `"facebook"`, `"amazon"`, `"apple"`, or `"oidc"` | Yes | Provider type | +| `name` | `string` | For OIDC | Provider name in Cognito (e.g., `"EntraID"`) | +| `label` | `string` | Yes | Button text on the login screen | +| `providerDetails` | `object` | Yes | AWS Cognito provider details — varies by type | +| `attributeMapping` | `object` | No | Override default claim-to-attribute mapping | + + + +For OIDC providers, `providerDetails` must include `client_id`, `client_secret`, and `oidc_issuer`. For social providers (Google, Facebook, etc.), see the [AWS documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-social-idp.html) for the required fields. + + + +## SSO-Only Mode + +To hide the email/password form and show only the federated provider buttons, set `allowCredentialsLogin: false`: + +```tsx + +``` + +The login screen will display a description telling users they'll be redirected to an external service. + +## Custom Identity Mapping + +By default, federated users are created in Webiny with the `full-access` role. To control how token claims map to Webiny roles and teams, provide an `apiConfig` extension: + +```tsx webiny.config.tsx + +``` + +```typescript extensions/cognito/api.ts +import { CognitoIdpConfig } from "@webiny/cognito"; + +class MyConfig implements CognitoIdpConfig.Interface { + getIdentity(token: CognitoIdpConfig.JwtPayload) { + const groups: string[] = (token["cognito:groups"] as string[]) || []; + + return { + roles: groups.includes("admins") ? ["full-access"] : ["content-editor"], + teams: groups.filter(g => g.startsWith("team-")), + }; + } +} + +export default CognitoIdpConfig.createImplementation({ + implementation: MyConfig, + dependencies: [] +}); +``` + + + +You only need to return the fields you want to override. Default values for `id`, `displayName`, and `profile` are automatically derived from standard Cognito token claims (`custom:id`, `given_name`, `family_name`, `email`). + + + +## Custom Login Screen Behavior + +For advanced login screen customization — such as IP-based credential whitelisting or custom provider buttons — provide an `adminConfig` extension that implements `CognitoSignInConfig`: + +```tsx webiny.config.tsx + +``` + +```tsx extensions/cognito/admin.tsx +import { CognitoSignInConfig } from "@webiny/cognito/admin"; + +class MySignInConfig implements CognitoSignInConfig.Interface { + async getConfig() { + return { + oauth: { + scopes: ["profile", "email", "openid"], + redirectSignIn: [window.location.origin], + redirectSignOut: [window.location.origin], + responseType: "code" as const, + }, + allowCredentialsLogin: false, + providers: [ + { name: "EntraID", label: "Sign in with Microsoft" } + ], + title: "Welcome", + description: "Use your corporate credentials to sign in.", + }; + } +} + +export default CognitoSignInConfig.createImplementation({ + implementation: MySignInConfig, + dependencies: [] +}); +``` + +Because `getConfig()` is async, you can perform runtime checks before returning the config: + +```tsx extensions/cognito/admin.tsx +import { CognitoSignInConfig } from "@webiny/cognito/admin"; + +const ALLOWED_IPS = ["1.2.3.4", "5.6.7.8"]; + +class MySignInConfig implements CognitoSignInConfig.Interface { + async getConfig() { + let allowCredentials = false; + if (process.env.REACT_APP_STAGE !== "prod") { + const res = await fetch("https://api64.ipify.org?format=json"); + const { ip } = await res.json(); + allowCredentials = ALLOWED_IPS.includes(ip); + } + + return { + oauth: { + scopes: ["profile", "email", "openid"], + redirectSignIn: [window.location.origin], + redirectSignOut: [window.location.origin], + responseType: "code" as const, + }, + allowCredentialsLogin: allowCredentials, + providers: [ + { name: "EntraID", label: "Sign in with Microsoft" } + ], + }; + } +} + +export default CognitoSignInConfig.createImplementation({ + implementation: MySignInConfig, + dependencies: [] +}); +``` + +## Setting It Up with AI + +If you have the Webiny MCP server connected, you can configure federation with a single prompt: + +> Set up Cognito Federation with Microsoft Entra ID. My tenant ID is `0ae3d912-...`, client ID is `f62ee823-...`, and client secret is `~Hp8Q~...`. Use SSO-only mode — no password login. Map all federated users to the `full-access` role. + +The AI assistant has access to the `webiny-cognito-federation` and `webiny-configure-entraid` skills and will generate the complete configuration. + +## Deploy Order + +1. **Deploy Core** — creates the Cognito User Pool Domain and Identity Provider resources + ```bash + yarn webiny deploy core --env=dev + ``` + +2. **Configure the external IdP** — update the redirect URI in your identity provider's settings to: + `https://{cognitoUserPoolDomain}/oauth2/idpresponse` + (get the domain from `yarn webiny output core --env=dev`) + +3. **Deploy API + Admin** + ```bash + yarn webiny deploy api --env=dev + yarn webiny deploy admin --env=dev + ``` diff --git a/docs/release-notes/6.4.4/changelog.ai.txt b/docs/release-notes/6.4.4/changelog.ai.txt new file mode 100644 index 000000000..06d34f3be --- /dev/null +++ b/docs/release-notes/6.4.4/changelog.ai.txt @@ -0,0 +1,11 @@ +AI Context: 6.4.4 Changelog (changelog.mdx) + +This file tracks manual edits made after the generation script ran. +The script reads the "Skipped PRs" section to avoid re-adding removed entries. + +## Skipped PRs + +## Manual Rewrites + +- #5373: Manually written entry for security fix (prototype pollution in `transformWhereToNested`). PR description was minimal ("Security issue: second-order prototype pollution vulnerability"), so the changelog entry was written from the diff to explain the attack vector and the two-part fix. +- #5447: Manually added — the generation script missed it. Backport of #5445 fixing scheduled CMS entry publish/unpublish failing with a `ValidationException` (invalid EventBridge Scheduler name). Entry rewritten user-facing from the detailed PR description. diff --git a/docs/release-notes/6.4.4/changelog.mdx b/docs/release-notes/6.4.4/changelog.mdx new file mode 100644 index 000000000..cf56167c4 --- /dev/null +++ b/docs/release-notes/6.4.4/changelog.mdx @@ -0,0 +1,117 @@ +--- +id: k5mqyohu +title: Webiny 6.4.4 Changelog +description: See what's new in Webiny version 6.4.4 +--- + +import { GithubRelease } from "@/components/GithubRelease"; +import { Alert } from "@/components/Alert"; + + + +## Infrastructure + +### Custom Production Environments Are Now Recognized During Deployment ([#5371](https://github.com/webiny/webiny-js/pull/5371)) + +Marking an environment as a production environment via the `Infra.ProductionEnvironments` setting in `webiny.config.tsx` previously had no effect — only the built-in `prod` and `production` names were treated as production, so custom environments (like `stage`) never received production-grade infrastructure such as a VPC. Configured production environments are now correctly recognized and deployed with the appropriate production setup. + + + +If an environment was already deployed before being marked as production, the next deploy will create the VPC, move existing Lambdas into it, add VPC endpoints, and enable resource protection. This is a disruptive infrastructure migration that may replace resources and cause downtime. Review `pulumi preview` carefully and deploy during a maintenance window. + + + +## Development + +### Faster Stack Output Reads via Local Caching ([#5375](https://github.com/webiny/webiny-js/pull/5375)) + +Every stack output lookup previously ran a Pulumi command, which made repeated reads slow—especially during deploys, watch mode, and output lookups that query the same stack multiple times. Stack outputs are now cached locally (under `.webiny/caches/stack-output`) and reused on subsequent reads. The cache is automatically cleared when an app is destroyed, so results always stay accurate. + +## Admin + +### Cognito Federation and Multi-Factor Authentication Support ([#5376](https://github.com/webiny/webiny-js/pull/5376)) + +Webiny now supports federated sign-in through Amazon Cognito, allowing users to authenticate via external identity providers such as Google, Facebook, Apple, Amazon, and any OIDC-compliant provider (e.g., Microsoft Entra ID). This eliminates the need for users to manage separate Webiny credentials when your organization already has an identity provider in place. + +To enable federation, use the `federation` prop on the `` extension: + +```typescript +import { Cognito } from "webiny/api-security-cognito"; + +export default createExtension({ + type: "api", + name: "api.security.cognito", + create() { + return [ + new Cognito({ + federation: { + providers: [ + { + type: "oidc", + name: "EntraID", + label: "Sign in with Microsoft" + } + ] + } + }) + ]; + } +}); +``` + +Federated users are automatically detected via the `identities` claim in the JWT token and flagged as external, triggering the `ExternalIdpUserSyncHandler` for identity mapping and provisioning. + +The login screen can be customized through the `adminConfig` extension point to control which providers appear, whether username/password credentials are shown, and to set custom titles or descriptions. + + + +For new deployments, the `custom:id` attribute maximum length has been increased to 256 characters to accommodate longer OIDC `sub` values from some identity providers. Existing deployments retain the 36-character limit. + + + +## Headless CMS + +### Unified OpenSearch Index Abstraction for CMS Models ([#5436](https://github.com/webiny/webiny-js/pull/5436)) + +A new `CmsModelOpenSearchIndex` abstraction gives you per-model control over OpenSearch index settings and shared-index behavior. Previously, index configuration relied on a last-wins pattern that made it difficult to override settings for specific models. The new approach uses the decorator pattern, allowing you to control whether a model uses a shared index across tenants or gets its own isolated index. + +```typescript +import { CmsModelOpenSearchIndex } from "webiny/api-headless-cms-ddb-es"; + +CmsModelOpenSearchIndex.createDecorator({ + decorator: class CustomIndex implements CmsModelOpenSearchIndex.Interface { + constructor(private original: CmsModelOpenSearchIndex.Interface) {} + + async execute(params: CmsModelOpenSearchIndex.Params): Promise { + const result = await this.original.execute(params); + // Force per-tenant index for models tagged with "isolated" + if (params.model.tags?.includes("isolated")) { + return { ...result, shared: false }; + } + return result; + } + }, + dependencies: [] +}); +``` + +This replaces the old `CmsEntryOpenSearchIndex` abstraction — if you were using it, migrate to the new decorator-based approach. + +### Fixed Prototype Pollution Vulnerability in Where Condition Parsing ([#5373](https://github.com/webiny/webiny-js/pull/5373)) + +A second-order prototype pollution vulnerability was found in the Headless CMS GraphQL `where` condition parser. Sending a crafted dot-notation key like `toString.call` as a filter key could corrupt built-in JavaScript prototypes (e.g., `Object.prototype.toString`), potentially affecting all subsequent requests in the same Lambda invocation. + +The fix introduces two defenses: + +- Keys like `__proto__`, `constructor`, and `prototype` are now explicitly forbidden and will throw an error. +- The dot-notation expander now uses `Object.hasOwn()` instead of a truthiness check, preventing inherited prototype methods from being mistaken for existing nested objects. + + + +This is a security fix. Upgrade as soon as possible. No action is required beyond upgrading - the fix is fully internal. + + + +### Fixed Scheduled Publishing and Unpublishing of CMS Entries ([#5447](https://github.com/webiny/webiny-js/pull/5447)) + +Scheduling a publish or unpublish action on a CMS entry failed with a `ValidationException`, because the generated AWS EventBridge Scheduler name contained illegal characters and exceeded the 64-character limit. Scheduled publish and unpublish actions now work as expected. diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx new file mode 100644 index 000000000..2e6b0d7fe --- /dev/null +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -0,0 +1,61 @@ +--- +id: 196v4u6s +title: Upgrade from 6.4.x to 6.4.4 +description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. +--- + +import { Alert } from "@/components/Alert"; +import { AdditionalNotes } from "@/components/upgrade/AdditionalNotes"; + + + +- how to upgrade Webiny from 6.4.x to 6.4.4 + + + + + +Make sure to check out the [6.4.4 changelog](./changelog) to get familiar with the changes introduced in this release. + + + +## Step-by-Step Guide + +### 1. Upgrade Webiny Packages + +Upgrade all Webiny packages by running the following command: + +```bash +yarn webiny upgrade 6.4.4 --debug +``` + +Note that the command above will run upgrades for all available versions of Webiny up to 6.4.4. If there are upgrades for 6.4.1, 6.4.5, they will be ran. + +You can omit the version to upgrade to the latest available: + +```bash +yarn webiny upgrade --debug +``` + +Once the upgrade has finished, running the `yarn webiny --version` command in your terminal should return **6.4.4**. + + + +If the above command fails or is not available in your setup, you can run the upgrade script directly via `npx`: + +```bash +npx https://github.com/webiny/webiny-upgrades-v6 6.4.4 --debug +``` + + + +### 2. Deploy Your Project + +Proceed by redeploying your Webiny project: + +```bash +# Execute in your project root. +yarn webiny deploy --env {environment} +``` + + diff --git a/docs/release-notes/6.4.5/changelog.ai.txt b/docs/release-notes/6.4.5/changelog.ai.txt new file mode 100644 index 000000000..a6e1fee41 --- /dev/null +++ b/docs/release-notes/6.4.5/changelog.ai.txt @@ -0,0 +1,8 @@ +AI Context: 6.4.5 Changelog (changelog.mdx) + +This file tracks manual edits made after the generation script ran. +The script reads the "Skipped PRs" section to avoid re-adding removed entries. + +## Skipped PRs + +## Manual Rewrites diff --git a/docs/release-notes/6.4.5/changelog.mdx b/docs/release-notes/6.4.5/changelog.mdx new file mode 100644 index 000000000..990508d37 --- /dev/null +++ b/docs/release-notes/6.4.5/changelog.mdx @@ -0,0 +1,35 @@ +--- +id: kkn9hcba +title: Webiny 6.4.5 Changelog +description: See what's new in Webiny version 6.4.5 +--- + +import { GithubRelease } from "@/components/GithubRelease"; + + + +## Admin + +### Custom Color Picker Support in the Lexical Editor + +The Lexical editor's font color toolbar action now supports a free form color picker in addition to the predefined color palette. You can enable it through the new `AllowCustomColors` admin config component: + +```tsx +import { AdminConfig } from "webiny/admin"; + + + +; +``` +This enables custom color picker globally, in both CMS and Website Builder. + +Alternatively, you can enable it per theme via the `allowCustomColors` property on the Website Builder theme config: + +```tsx +import { createTheme } from "@webiny/website-builder-nextjs"; + +const theme = createTheme({ + // ...other theme options + allowCustomColors: true +}); +``` diff --git a/docs/release-notes/6.4.5/upgrade-guide.mdx b/docs/release-notes/6.4.5/upgrade-guide.mdx new file mode 100644 index 000000000..832a614b1 --- /dev/null +++ b/docs/release-notes/6.4.5/upgrade-guide.mdx @@ -0,0 +1,61 @@ +--- +id: vhi9nju0 +title: Upgrade from 6.4.x to 6.4.5 +description: Learn how to upgrade Webiny from 6.4.x to 6.4.5. +--- + +import { Alert } from "@/components/Alert"; +import { AdditionalNotes } from "@/components/upgrade/AdditionalNotes"; + + + +- how to upgrade Webiny from 6.4.x to 6.4.5 + + + + + +Make sure to check out the [6.4.5 changelog](./changelog) to get familiar with the changes introduced in this release. + + + +## Step-by-Step Guide + +### 1. Upgrade Webiny Packages + +Upgrade all Webiny packages by running the following command: + +```bash +yarn webiny upgrade 6.4.5 --debug +``` + +Note that the command above will run upgrades for all available versions of Webiny up to 6.4.5. If there are upgrades for 6.4.1, 6.4.5, they will be ran. + +You can omit the version to upgrade to the latest available: + +```bash +yarn webiny upgrade --debug +``` + +Once the upgrade has finished, running the `yarn webiny --version` command in your terminal should return **6.4.5**. + + + +If the above command fails or is not available in your setup, you can run the upgrade script directly via `npx`: + +```bash +npx https://github.com/webiny/webiny-upgrades-v6 6.4.5 --debug +``` + + + +### 2. Deploy Your Project + +Proceed by redeploying your Webiny project: + +```bash +# Execute in your project root. +yarn webiny deploy --env {environment} +``` + + diff --git a/package.json b/package.json index ee59d035f..bf8ae5086 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "prepare-release-notes": "node scripts/prepareReleaseNotes.js", "get-id": "node -e 'console.log(require(\"@webiny/utils\").mdbid().slice(-8))'", "validate:mdx": "tsx scripts/validate-mdx-pairing.ts", + "check:changelog-review": "tsx scripts/check-changelog-review.ts", "generate-sdk-reference": "tsx scripts/generate-sdk-reference.ts", "generate:reference": "tsx scripts/generate-reference.ts", "generate:changelog": "tsx scripts/generate-changelog.ts", diff --git a/scripts/changelog-constants.ts b/scripts/changelog-constants.ts new file mode 100644 index 000000000..1aca9efde --- /dev/null +++ b/scripts/changelog-constants.ts @@ -0,0 +1,10 @@ +/** + * Shared constants for the changelog generation and review-check scripts. + */ + +/** + * Sentinel embedded in per-item review markers. The generator writes one MDX + * comment containing this string beneath every generated changelog entry; the + * CI check greps for it and fails while any remain unresolved. + */ +export const REVIEW_MARKER = "REVIEW-PENDING"; diff --git a/scripts/check-changelog-review.ts b/scripts/check-changelog-review.ts new file mode 100644 index 000000000..2439a05a4 --- /dev/null +++ b/scripts/check-changelog-review.ts @@ -0,0 +1,83 @@ +/** + * Changelog Review-Marker Check + * + * The changelog generator inserts a per-item review marker (an MDX comment + * containing the PR author's handle) beneath every generated entry. Each author + * is expected to inspect their entry and delete the marker line to confirm it. + * + * This script fails if any changelog still contains an unresolved marker, so a + * release-notes PR cannot be merged/published until every entry has been + * confirmed. Run in CI on pull requests targeting master. + * + * Usage: + * yarn tsx scripts/check-changelog-review.ts # scan all releases + * yarn tsx scripts/check-changelog-review.ts --version 6.4.4 + * + * Exit codes: + * 0 - No unresolved review markers + * 1 - Unresolved review markers found + */ + +import { readFileSync } from "fs"; +import globby from "globby"; +import { REVIEW_MARKER } from "./changelog-constants"; + +interface Finding { + file: string; + line: number; + text: string; +} + +function parseArgs(): { version?: string } { + const args = process.argv.slice(2); + const idx = args.indexOf("--version"); + if (idx !== -1 && args[idx + 1]) { + return { version: args[idx + 1] }; + } + return {}; +} + +async function main(): Promise { + const { version } = parseArgs(); + + const pattern = version + ? `docs/release-notes/${version}/changelog.mdx` + : "docs/release-notes/**/changelog.mdx"; + + const files = await globby(pattern); + + if (files.length === 0) { + console.warn(`No changelog files matched: ${pattern}`); + process.exit(0); + } + + const findings: Finding[] = []; + for (const file of files) { + const lines = readFileSync(file, "utf-8").split("\n"); + lines.forEach((text, i) => { + if (text.includes(REVIEW_MARKER)) { + findings.push({ file, line: i + 1, text: text.trim() }); + } + }); + } + + if (findings.length === 0) { + console.log(`✓ No unresolved "${REVIEW_MARKER}" markers in ${files.length} changelog(s).`); + process.exit(0); + } + + console.error(`✗ Found ${findings.length} unresolved "${REVIEW_MARKER}" marker(s):\n`); + for (const f of findings) { + console.error(` ${f.file}:${f.line}`); + console.error(` ${f.text}`); + } + console.error( + `\nEach author must inspect their changelog entry and delete its ${REVIEW_MARKER} line before the release notes can be published.` + ); + process.exit(1); +} + +main().catch(err => { + console.error("\nError:", err instanceof Error ? err.message : err); + process.exit(2); +}); diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index 3275d453a..52bcf2852 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -4,6 +4,9 @@ * 2. Sending them to Claude (claude-opus-4-5) to produce a structured MDX changelog * 3. Writing the result to docs/release-notes/{version}/changelog.mdx * + * PR discovery is milestone-based: assign a PR to the `X.Y.Z` milestone for it to + * appear in that release's changelog. PRs labeled `no-changelog` are skipped. + * * Usage: * yarn tsx scripts/generate-changelog.ts --version 6.1.0 * @@ -18,26 +21,28 @@ import "dotenv/config"; import Anthropic from "@anthropic-ai/sdk"; import { writeFileSync, mkdirSync, readFileSync } from "fs"; import { join } from "path"; +import { REVIEW_MARKER } from "./changelog-constants"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- +interface GitHubLabel { + name: string; +} + interface GitHubMilestone { number: number; title: string; state: string; } -interface GitHubLabel { - name: string; -} - interface GitHubIssue { number: number; title: string; body: string | null; labels: GitHubLabel[]; + user: { login: string } | null; pull_request?: unknown; state: string; } @@ -47,21 +52,24 @@ interface PullRequest { title: string; body: string; url: string; + author: string; } // --------------------------------------------------------------------------- // CLI args // --------------------------------------------------------------------------- -function parseArgs(): { version: string } { +function parseArgs(): { version: string; dryRun: boolean } { const args = process.argv.slice(2); const versionIdx = args.indexOf("--version"); if (versionIdx === -1 || !args[versionIdx + 1]) { - console.error("Usage: yarn tsx scripts/generate-changelog.ts --version "); + console.error( + "Usage: yarn tsx scripts/generate-changelog.ts --version [--dry-run]" + ); console.error("Example: yarn tsx scripts/generate-changelog.ts --version 6.1.0"); process.exit(1); } - return { version: args[versionIdx + 1] }; + return { version: args[versionIdx + 1], dryRun: args.includes("--dry-run") }; } // --------------------------------------------------------------------------- @@ -72,10 +80,12 @@ const GITHUB_REPO = "webiny/webiny-js"; const GITHUB_API = "https://api.github.com"; async function fetchJson(url: string): Promise { + const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; const res = await fetch(url, { headers: { Accept: "application/vnd.github+json", - "User-Agent": "webiny-docs-changelog-generator" + "User-Agent": "webiny-docs-changelog-generator", + ...(token ? { Authorization: `Bearer ${token}` } : {}) } }); if (!res.ok) { @@ -84,6 +94,10 @@ async function fetchJson(url: string): Promise { return res.json() as Promise; } +/** + * Finds the milestone whose title matches the target version (`X.Y.Z` or `vX.Y.Z`). + * Exits cleanly with a message if no matching milestone exists. + */ async function findMilestone(version: string): Promise { const bare = version.replace(/^v/, ""); const candidates = [bare, `v${bare}`]; @@ -121,7 +135,9 @@ async function fetchPRsForMilestone(milestoneNumber: number): Promise l.name === "no-changelog"); if (hasNoChangelog) { console.log(` Skipping PR #${issue.number} (no-changelog): ${issue.title}`); @@ -132,7 +148,8 @@ async function fetchPRsForMilestone(milestoneNumber: number): Promise): string { + const lines = body.split("\n"); + const out: string[] = []; + + for (const line of lines) { + out.push(line); + if (!line.startsWith("### ")) continue; + + const prNumbers = [...line.matchAll(/\/pull\/(\d+)/g)].map(m => parseInt(m[1], 10)); + if (prNumbers.length === 0) continue; + + const authors = [...new Set(prNumbers.map(n => authorByPR.get(n)).filter(Boolean))]; + if (authors.length === 0) continue; + + const handles = authors.map(a => `@${a}`).join(" "); + out.push(`{/* ${REVIEW_MARKER} ${handles} — confirm this entry, then delete this line */}`); + } + + return out.join("\n"); +} + // --------------------------------------------------------------------------- // MDX file builder // --------------------------------------------------------------------------- @@ -415,7 +466,7 @@ function buildMdxFile(version: string, body: string): string { // --------------------------------------------------------------------------- async function main(): Promise { - const { version } = parseArgs(); + const { version, dryRun } = parseArgs(); console.log(`\nGenerating changelog for Webiny ${version}...`); @@ -425,7 +476,20 @@ async function main(): Promise { console.log(" Fetching merged PRs..."); const { prs, noChangelogNumbers } = await fetchPRsForMilestone(milestone.number); - console.log(` Found ${prs.length} pull requests.`); + console.log(` ${prs.length} pull request(s) eligible for the changelog.`); + + if (dryRun) { + console.log("\n --dry-run: eligible PRs (no Claude call, no files written):"); + for (const pr of prs) { + console.log(` #${pr.number}: ${pr.title}`); + } + if (noChangelogNumbers.size > 0) { + console.log( + ` Skipped (no-changelog): ${[...noChangelogNumbers].map(n => `#${n}`).join(", ")}` + ); + } + process.exit(0); + } const outDir = join(process.cwd(), "docs", "release-notes", version); mkdirSync(outDir, { recursive: true }); @@ -472,7 +536,9 @@ async function main(): Promise { ); } - const body = await generateChangelogBody(newPRs, version); + const rawBody = await generateChangelogBody(newPRs, version); + const authorByPR = new Map(newPRs.map(pr => [pr.number, pr.author])); + const body = addReviewMarkers(rawBody, authorByPR); if (alreadyPresent.size > 0) { const existing = readFileSync(outPath, "utf-8");