From 169f76c00ffe31859d47e50c1a5b2a2b5f627b66 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 1 Jul 2026 16:09:39 +0000 Subject: [PATCH 01/28] chore: generate changelog for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/release-notes/6.4.4/upgrade-guide.mdx 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..a737addcd --- /dev/null +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -0,0 +1,61 @@ +--- +id: jftwvjv6 +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} +``` + + From a614eba9915032f1006ef6159f444cef28e0f865 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 1 Jul 2026 16:14:15 +0000 Subject: [PATCH 02/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index a737addcd..2c470d484 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: jftwvjv6 +id: m3lg57w7 title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 0aeba435a19e4b6a5c1c097a0738cf9f58e2bb78 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 1 Jul 2026 16:18:29 +0000 Subject: [PATCH 03/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 2c470d484..71f315740 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: m3lg57w7 +id: q7piuxnc title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From d97a4f798eea249c3cd3dec3b050f01c08cf78e0 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Fri, 3 Jul 2026 08:25:45 +0000 Subject: [PATCH 04/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 71f315740..352415134 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: q7piuxnc +id: 3aac8x7l title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 53d13bd4223fac901f380f74a27cd9f2a5580bbc Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Mon, 6 Jul 2026 18:23:15 +0000 Subject: [PATCH 05/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 352415134..7d8b6ecd8 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: 3aac8x7l +id: d1g1e64z title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 82d0a756e25e95a443515dc41a2153151dc87cfa Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Tue, 7 Jul 2026 12:02:30 +0000 Subject: [PATCH 06/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/changelog.ai.txt | 8 ++++++++ docs/release-notes/6.4.4/changelog.mdx | 22 ++++++++++++++++++++++ docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/release-notes/6.4.4/changelog.ai.txt create mode 100644 docs/release-notes/6.4.4/changelog.mdx 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..6853f5769 --- /dev/null +++ b/docs/release-notes/6.4.4/changelog.ai.txt @@ -0,0 +1,8 @@ +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 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..158a4f8fb --- /dev/null +++ b/docs/release-notes/6.4.4/changelog.mdx @@ -0,0 +1,22 @@ +--- +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. + + diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 7d8b6ecd8..a4e0ba5bd 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: d1g1e64z +id: 4gxx5tkd title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From b82f8e62b66375410cd4c624e7aec0152d4e0de0 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Tue, 7 Jul 2026 19:42:34 +0000 Subject: [PATCH 07/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/changelog.mdx | 6 ++++++ docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/changelog.mdx b/docs/release-notes/6.4.4/changelog.mdx index 158a4f8fb..0e9059e2e 100644 --- a/docs/release-notes/6.4.4/changelog.mdx +++ b/docs/release-notes/6.4.4/changelog.mdx @@ -20,3 +20,9 @@ Marking an environment as a production environment via the `Infra.ProductionEnvi 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. diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index a4e0ba5bd..3c31ad1bc 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: 4gxx5tkd +id: sccccu9g title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From d1e02954dade142f82854112af865ea213ca40dd Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Tue, 7 Jul 2026 21:49:33 +0200 Subject: [PATCH 08/28] chore: add cognito federation article --- docs/developer-docs/6.x/navigation.tsx | 1 + .../6.x/security/cognito-federation.ai.txt | 45 ++++ .../6.x/security/cognito-federation.mdx | 255 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 docs/developer-docs/6.x/security/cognito-federation.ai.txt create mode 100644 docs/developer-docs/6.x/security/cognito-federation.mdx 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 + ``` From bfb3be8a613e7938094ca01acbc0eae402836ae1 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 8 Jul 2026 09:27:14 +0000 Subject: [PATCH 09/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/changelog.mdx | 42 ++++++++++++++++++++++ docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/changelog.mdx b/docs/release-notes/6.4.4/changelog.mdx index 0e9059e2e..64160cead 100644 --- a/docs/release-notes/6.4.4/changelog.mdx +++ b/docs/release-notes/6.4.4/changelog.mdx @@ -26,3 +26,45 @@ If an environment was already deployed before being marked as production, the ne ### 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. + + diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 3c31ad1bc..7b7bb2959 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: sccccu9g +id: l3s6kum0 title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 1e06f9fcb4a52209e765ae3d65682d41fde1a8c1 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 8 Jul 2026 10:13:54 +0000 Subject: [PATCH 10/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 7b7bb2959..b9c2240cf 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: l3s6kum0 +id: nkk7h76z title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 782457a36c53f8da0cbf6925bbab8731c7fcdb73 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 8 Jul 2026 12:15:29 +0000 Subject: [PATCH 11/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index b9c2240cf..8a96adfbd 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: nkk7h76z +id: u2q4s6pp title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From b699f1e0ad4d160614cb028a8b764337edcb726c Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 8 Jul 2026 12:25:33 +0000 Subject: [PATCH 12/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 8a96adfbd..e7f50b0c8 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: u2q4s6pp +id: br21vkap title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From ac5fcc5aa0f5f6affdcaa6512a1044e2a279edac Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 8 Jul 2026 18:34:49 +0000 Subject: [PATCH 13/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index e7f50b0c8..df03195f4 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: br21vkap +id: ax8jonkp title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 2e70dc1b14d1390f34c8a9035ffacda5c0bb1fed Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Wed, 8 Jul 2026 20:10:28 +0000 Subject: [PATCH 14/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index df03195f4..278596270 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: ax8jonkp +id: urady4cx title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 2072ae0a41007644fdbb3a64b6fb021f10561227 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Mon, 13 Jul 2026 07:53:11 +0000 Subject: [PATCH 15/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 278596270..3ffb23ec7 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: urady4cx +id: cj9h1f9j title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 91a7ae417aacb66df1b931c20eed686609ef531e Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Fri, 17 Jul 2026 13:01:15 +0000 Subject: [PATCH 16/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 3ffb23ec7..815b076c2 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: cj9h1f9j +id: zml17rgf title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From f12da65c1737e3c3b3e1d4ac9e4c6b0ac0ddcdb0 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Mon, 20 Jul 2026 16:20:47 +0000 Subject: [PATCH 17/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 815b076c2..bf61fe14d 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: zml17rgf +id: ivj89cj4 title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 9536ca88a7d3298594c969b6ccaabeb028a06884 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Mon, 20 Jul 2026 17:01:53 +0000 Subject: [PATCH 18/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/changelog.mdx | 28 ++++++++++++++++++++++ docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/changelog.mdx b/docs/release-notes/6.4.4/changelog.mdx index 64160cead..cf466a6c1 100644 --- a/docs/release-notes/6.4.4/changelog.mdx +++ b/docs/release-notes/6.4.4/changelog.mdx @@ -68,3 +68,31 @@ The login screen can be customized through the `adminConfig` extension point to 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. diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index bf61fe14d..5de010547 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: ivj89cj4 +id: 5bx0rov2 title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From b8e078132c95e3a574b7f8bfe646e3fa4e761769 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Mon, 20 Jul 2026 20:01:13 +0000 Subject: [PATCH 19/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index 5de010547..a866b5441 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: 5bx0rov2 +id: pef3n8de title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From a5fbbeb355f4779aedf2355f806ccd1bcda76862 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Mon, 20 Jul 2026 22:48:19 +0000 Subject: [PATCH 20/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index a866b5441..a26a96bad 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: pef3n8de +id: bhix13jl title: Upgrade from 6.4.x to 6.4.4 description: Learn how to upgrade Webiny from 6.4.x to 6.4.4. --- From 05d09e3e032d10e9ff3e0681023b960c508b9207 Mon Sep 17 00:00:00 2001 From: webiny-bot Date: Tue, 21 Jul 2026 07:24:44 +0000 Subject: [PATCH 21/28] chore: regenerate release notes for 6.4.4 --- docs/release-notes/6.4.4/upgrade-guide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/6.4.4/upgrade-guide.mdx b/docs/release-notes/6.4.4/upgrade-guide.mdx index a26a96bad..2e6b0d7fe 100644 --- a/docs/release-notes/6.4.4/upgrade-guide.mdx +++ b/docs/release-notes/6.4.4/upgrade-guide.mdx @@ -1,5 +1,5 @@ --- -id: bhix13jl +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. --- From a4474bf936193dd15d74406deab8292ea519b03e Mon Sep 17 00:00:00 2001 From: brunozoric Date: Tue, 21 Jul 2026 12:56:21 +0200 Subject: [PATCH 22/28] docs: add security fix entry for prototype pollution (#5373) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/release-notes/6.4.4/changelog.ai.txt | 2 ++ docs/release-notes/6.4.4/changelog.mdx | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/docs/release-notes/6.4.4/changelog.ai.txt b/docs/release-notes/6.4.4/changelog.ai.txt index 6853f5769..1593ecaea 100644 --- a/docs/release-notes/6.4.4/changelog.ai.txt +++ b/docs/release-notes/6.4.4/changelog.ai.txt @@ -6,3 +6,5 @@ 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. diff --git a/docs/release-notes/6.4.4/changelog.mdx b/docs/release-notes/6.4.4/changelog.mdx index cf466a6c1..7ffc0d31a 100644 --- a/docs/release-notes/6.4.4/changelog.mdx +++ b/docs/release-notes/6.4.4/changelog.mdx @@ -96,3 +96,18 @@ CmsModelOpenSearchIndex.createDecorator({ ``` 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. + + From 4da540d41db665542e65cc19213ef961fe704cf1 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Tue, 21 Jul 2026 13:36:23 +0200 Subject: [PATCH 23/28] docs: add changelog entry for scheduled CMS entry fix (#5447) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/release-notes/6.4.4/changelog.ai.txt | 1 + docs/release-notes/6.4.4/changelog.mdx | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/release-notes/6.4.4/changelog.ai.txt b/docs/release-notes/6.4.4/changelog.ai.txt index 1593ecaea..06d34f3be 100644 --- a/docs/release-notes/6.4.4/changelog.ai.txt +++ b/docs/release-notes/6.4.4/changelog.ai.txt @@ -8,3 +8,4 @@ The script reads the "Skipped PRs" section to avoid re-adding removed entries. ## 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 index 7ffc0d31a..cf56167c4 100644 --- a/docs/release-notes/6.4.4/changelog.mdx +++ b/docs/release-notes/6.4.4/changelog.mdx @@ -111,3 +111,7 @@ The fix introduces two defenses: 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. From 59f8826b003079541f0d940811f0cb2076e41ffe Mon Sep 17 00:00:00 2001 From: adrians5j Date: Wed, 22 Jul 2026 07:09:29 +0200 Subject: [PATCH 24/28] chore: discover changelog PRs from release commits instead of milestone Parse (#NNNN) references from commits between the previous release tag and this version's tag (falling back to the X.Y.x branch when untagged), so a release still gets a complete changelog even if PRs were never milestoned. Adds a --dry-run flag and optional GITHUB_TOKEN auth. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/generate-changelog.ts | 224 +++++++++++++++++++++++++--------- 1 file changed, 168 insertions(+), 56 deletions(-) diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index 3275d453a..5cee5b588 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -1,9 +1,14 @@ /** * Generates a changelog MDX file for a given Webiny release version by: - * 1. Fetching all closed PRs from the webiny/webiny-js milestone matching the version - * 2. Sending them to Claude (claude-opus-4-5) to produce a structured MDX changelog + * 1. Diffing the previous release tag against this version's tag on webiny/webiny-js + * and extracting the PR numbers referenced in the commit messages (`(#1234)`) + * 2. Fetching each PR and 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 commit-based (not milestone-based), so a release still gets a + * complete changelog even if PRs were never assigned to a milestone. + * * Usage: * yarn tsx scripts/generate-changelog.ts --version 6.1.0 * @@ -23,23 +28,27 @@ import { join } from "path"; // Types // --------------------------------------------------------------------------- -interface GitHubMilestone { - number: number; - title: string; - state: string; +interface GitHubLabel { + name: string; } -interface GitHubLabel { +interface GitHubTag { name: string; } -interface GitHubIssue { +interface GitHubCommit { + commit: { message: string }; +} + +interface GitHubCompare { + commits: GitHubCommit[]; +} + +interface GitHubPull { number: number; title: string; body: string | null; labels: GitHubLabel[]; - pull_request?: unknown; - state: string; } interface PullRequest { @@ -53,15 +62,17 @@ interface PullRequest { // 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 +83,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,24 +97,109 @@ async function fetchJson(url: string): Promise { return res.json() as Promise; } -async function findMilestone(version: string): Promise { - const bare = version.replace(/^v/, ""); - const candidates = [bare, `v${bare}`]; +/** + * Like fetchJson, but returns null on 404 instead of throwing — used to probe + * for a version tag that may not have been created yet. + */ +async function fetchJsonOrNull(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", + ...(token ? { Authorization: `Bearer ${token}` } : {}) + } + }); + if (res.status === 404) return null; + if (!res.ok) { + throw new Error(`GitHub API error ${res.status} for ${url}: ${await res.text()}`); + } + return res.json() as Promise; +} +/** + * Compares two semver strings. Returns >0 if a>b, <0 if a v.replace(/^v/, "").split(".").map(n => parseInt(n, 10) || 0); + const pa = parse(a); + const pb = parse(b); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] ?? 0) - (pb[i] ?? 0); + if (diff !== 0) return diff; + } + return 0; +} + +/** + * Finds the release tag immediately preceding `version` by fetching all tags and + * picking the highest one that is strictly lower than the target version. + */ +async function findPreviousTag(version: string): Promise { + const target = version.replace(/^v/, ""); + const semverTag = /^v?\d+\.\d+\.\d+$/; + + const tags: string[] = []; let page = 1; while (true) { - const milestones = await fetchJson( - `${GITHUB_API}/repos/${GITHUB_REPO}/milestones?state=all&per_page=100&page=${page}` + const batch = await fetchJson( + `${GITHUB_API}/repos/${GITHUB_REPO}/tags?per_page=100&page=${page}` ); - if (milestones.length === 0) break; - - const found = milestones.find(m => candidates.includes(m.title)); - if (found) return found; + if (batch.length === 0) break; + tags.push(...batch.map(t => t.name)); page++; } - console.log("No milestone found."); - process.exit(0); + const previous = tags + .filter(t => semverTag.test(t)) + .filter(t => compareSemver(t, target) < 0) + .sort(compareSemver) + .pop(); + + if (!previous) { + throw new Error(`Could not determine the previous release tag for ${version}.`); + } + return previous; +} + +/** + * Resolves the "head" ref to diff against `base`. Prefers the `vX.Y.Z` tag; if it + * does not exist yet (release not tagged), falls back to the `X.Y.x` line branch. + */ +async function resolveHeadRef(version: string): Promise { + const bare = version.replace(/^v/, ""); + const versionTag = `v${bare}`; + + const tag = await fetchJsonOrNull( + `${GITHUB_API}/repos/${GITHUB_REPO}/git/ref/tags/${versionTag}` + ); + if (tag) return versionTag; + + const [major, minor] = bare.split("."); + const lineBranch = `${major}.${minor}.x`; + console.log( + ` Tag ${versionTag} not found — falling back to the ${lineBranch} branch. ` + + `Note: this may include commits from later patch releases on the same line.` + ); + return lineBranch; +} + +/** + * Extracts PR numbers referenced as `(#1234)` in the commit messages between two refs. + */ +async function fetchPRNumbersInRange(base: string, head: string): Promise { + const compare = await fetchJson( + `${GITHUB_API}/repos/${GITHUB_REPO}/compare/${base}...${head}` + ); + + const numbers = new Set(); + for (const c of compare.commits) { + for (const match of c.commit.message.matchAll(/\(#(\d+)\)/g)) { + numbers.add(parseInt(match[1], 10)); + } + } + return [...numbers].sort((a, b) => a - b); } interface FetchResult { @@ -109,33 +207,29 @@ interface FetchResult { noChangelogNumbers: Set; } -async function fetchPRsForMilestone(milestoneNumber: number): Promise { +async function fetchPRs(numbers: number[]): Promise { const prs: PullRequest[] = []; const noChangelogNumbers = new Set(); - let page = 1; - while (true) { - const issues = await fetchJson( - `${GITHUB_API}/repos/${GITHUB_REPO}/issues?milestone=${milestoneNumber}&state=closed&per_page=100&page=${page}` + for (const number of numbers) { + const pull = await fetchJsonOrNull( + `${GITHUB_API}/repos/${GITHUB_REPO}/pulls/${number}` ); - if (issues.length === 0) break; - - for (const issue of issues) { - if (!issue.pull_request) continue; - const hasNoChangelog = issue.labels.some(l => l.name === "no-changelog"); - if (hasNoChangelog) { - console.log(` Skipping PR #${issue.number} (no-changelog): ${issue.title}`); - noChangelogNumbers.add(issue.number); - continue; - } - prs.push({ - number: issue.number, - title: issue.title, - body: issue.body ?? "", - url: `https://github.com/${GITHUB_REPO}/pull/${issue.number}` - }); + // Referenced number that isn't actually a PR (e.g. an issue) — skip it. + if (!pull) continue; + + const hasNoChangelog = pull.labels.some(l => l.name === "no-changelog"); + if (hasNoChangelog) { + console.log(` Skipping PR #${pull.number} (no-changelog): ${pull.title}`); + noChangelogNumbers.add(pull.number); + continue; } - page++; + prs.push({ + number: pull.number, + title: pull.title, + body: pull.body ?? "", + url: `https://github.com/${GITHUB_REPO}/pull/${pull.number}` + }); } return { prs, noChangelogNumbers }; @@ -415,17 +509,35 @@ 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}...`); - console.log(" Looking up milestone on GitHub..."); - const milestone = await findMilestone(version); - console.log(` Found milestone #${milestone.number}: "${milestone.title}"`); - - console.log(" Fetching merged PRs..."); - const { prs, noChangelogNumbers } = await fetchPRsForMilestone(milestone.number); - console.log(` Found ${prs.length} pull requests.`); + console.log(" Resolving release range on GitHub..."); + const [previousTag, headRef] = await Promise.all([ + findPreviousTag(version), + resolveHeadRef(version) + ]); + console.log(` Diffing ${previousTag}...${headRef} for referenced PRs...`); + const prNumbers = await fetchPRNumbersInRange(previousTag, headRef); + console.log(` Found ${prNumbers.length} referenced PR(s) in commit messages.`); + + console.log(" Fetching PR details..."); + const { prs, noChangelogNumbers } = await fetchPRs(prNumbers); + 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 }); @@ -446,7 +558,7 @@ async function main(): Promise { } if (prs.length === 0) { - console.warn(" No pull requests found for this milestone. Nothing to generate."); + console.warn(" No pull requests found for this release range. Nothing to generate."); process.exit(0); } From b573a717f23e9d70c4bc471174a21f7ecf7c65f8 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Wed, 22 Jul 2026 07:30:17 +0200 Subject: [PATCH 25/28] feat: add per-item review markers to changelog + CI gate Each generated changelog entry now carries an MDX comment naming the PR author(s) ({/* REVIEW-PENDING @author ... */}). Authors delete the line to confirm their entry. A new CI workflow (check-release-notes.yml) fails any PR to master that still contains an unresolved marker, so release notes cannot be published until every entry is confirmed. - scripts/changelog-constants.ts: shared REVIEW_MARKER sentinel - scripts/check-changelog-review.ts + check:changelog-review script - generate-changelog.ts: capture PR author, inject markers deterministically Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/check-release-notes.yml | 31 +++++++++ AGENTS.md | 5 ++ package.json | 1 + scripts/changelog-constants.ts | 10 +++ scripts/check-changelog-review.ts | 83 +++++++++++++++++++++++ scripts/generate-changelog.ts | 44 +++++++++++- 6 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/check-release-notes.yml create mode 100644 scripts/changelog-constants.ts create mode 100644 scripts/check-changelog-review.ts 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/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 5cee5b588..ad1636189 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -23,6 +23,7 @@ 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 @@ -49,6 +50,7 @@ interface GitHubPull { title: string; body: string | null; labels: GitHubLabel[]; + user: { login: string } | null; } interface PullRequest { @@ -56,6 +58,7 @@ interface PullRequest { title: string; body: string; url: string; + author: string; } // --------------------------------------------------------------------------- @@ -228,7 +231,8 @@ async function fetchPRs(numbers: number[]): Promise { number: pull.number, title: pull.title, body: pull.body ?? "", - url: `https://github.com/${GITHUB_REPO}/pull/${pull.number}` + url: `https://github.com/${GITHUB_REPO}/pull/${pull.number}`, + author: pull.user?.login ?? "unknown" }); } @@ -480,6 +484,40 @@ function mergeDuplicateH2Sections(content: string): { merged: string; duplicates return { merged: result, duplicates }; } +// --------------------------------------------------------------------------- +// Review markers +// --------------------------------------------------------------------------- + +/** + * Inserts a per-item review marker (an MDX comment) directly beneath every H3 + * heading, attributing the item to the author(s) of the referenced PR(s). The + * marker is invisible in the rendered page but visible in the PR diff; reviewers + * delete the line to confirm the entry. CI fails while any marker remains. + * + * Markers are added deterministically after generation (not by Claude), so they + * stay correct regardless of how Claude grouped or reordered the PRs. + */ +function addReviewMarkers(body: string, authorByPR: Map): 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 // --------------------------------------------------------------------------- @@ -584,7 +622,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"); From f91ed90a1a4403183ea3662488a7e26f08d34996 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:22:47 +0200 Subject: [PATCH 26/28] Release 6.4.5 (#804) --- docs/release-notes/6.4.5/changelog.ai.txt | 8 +++ docs/release-notes/6.4.5/changelog.mdx | 35 +++++++++++++ docs/release-notes/6.4.5/upgrade-guide.mdx | 61 ++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 docs/release-notes/6.4.5/changelog.ai.txt create mode 100644 docs/release-notes/6.4.5/changelog.mdx create mode 100644 docs/release-notes/6.4.5/upgrade-guide.mdx 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} +``` + + From 1b7ce32376d2ec58bce80535efddf59fc3798329 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Fri, 31 Jul 2026 16:39:24 +0200 Subject: [PATCH 27/28] fix(changelog): fall back to release/X.Y.Z branch when version tag missing Co-Authored-By: Claude Opus 4.8 --- scripts/generate-changelog.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index ad1636189..e7f4e40cd 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -168,7 +168,7 @@ async function findPreviousTag(version: string): Promise { /** * Resolves the "head" ref to diff against `base`. Prefers the `vX.Y.Z` tag; if it - * does not exist yet (release not tagged), falls back to the `X.Y.x` line branch. + * does not exist yet (release not tagged), falls back to the `release/X.Y.Z` branch. */ async function resolveHeadRef(version: string): Promise { const bare = version.replace(/^v/, ""); @@ -179,13 +179,11 @@ async function resolveHeadRef(version: string): Promise { ); if (tag) return versionTag; - const [major, minor] = bare.split("."); - const lineBranch = `${major}.${minor}.x`; + const releaseBranch = `release/${bare}`; console.log( - ` Tag ${versionTag} not found — falling back to the ${lineBranch} branch. ` + - `Note: this may include commits from later patch releases on the same line.` + ` Tag ${versionTag} not found — falling back to the ${releaseBranch} branch.` ); - return lineBranch; + return releaseBranch; } /** From ea4ff8559c18f6868c55f80650f8d5dfcad1475a Mon Sep 17 00:00:00 2001 From: adrians5j Date: Fri, 31 Jul 2026 17:33:35 +0200 Subject: [PATCH 28/28] chore: discover changelog PRs from milestone instead of release commits Revert PR discovery to milestone-based lookup (assign a PR to the X.Y.Z milestone to include it), replacing the tag/branch commit-range diffing that broke when no version tag exists yet. Keeps GITHUB_TOKEN auth, --dry-run, and per-item review markers. Co-Authored-By: Claude Opus 4.8 --- scripts/generate-changelog.ts | 194 ++++++++++------------------------ 1 file changed, 55 insertions(+), 139 deletions(-) diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index e7f4e40cd..52bcf2852 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -1,13 +1,11 @@ /** * Generates a changelog MDX file for a given Webiny release version by: - * 1. Diffing the previous release tag against this version's tag on webiny/webiny-js - * and extracting the PR numbers referenced in the commit messages (`(#1234)`) - * 2. Fetching each PR and sending them to Claude (claude-opus-4-5) to produce a - * structured MDX changelog + * 1. Fetching all closed PRs from the webiny/webiny-js milestone matching the version + * 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 commit-based (not milestone-based), so a release still gets a - * complete changelog even if PRs were never assigned to a milestone. + * 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 @@ -33,24 +31,20 @@ interface GitHubLabel { name: string; } -interface GitHubTag { - name: string; -} - -interface GitHubCommit { - commit: { message: string }; -} - -interface GitHubCompare { - commits: GitHubCommit[]; +interface GitHubMilestone { + number: number; + title: string; + state: string; } -interface GitHubPull { +interface GitHubIssue { number: number; title: string; body: string | null; labels: GitHubLabel[]; user: { login: string } | null; + pull_request?: unknown; + state: string; } interface PullRequest { @@ -101,106 +95,27 @@ async function fetchJson(url: string): Promise { } /** - * Like fetchJson, but returns null on 404 instead of throwing — used to probe - * for a version tag that may not have been created yet. - */ -async function fetchJsonOrNull(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", - ...(token ? { Authorization: `Bearer ${token}` } : {}) - } - }); - if (res.status === 404) return null; - if (!res.ok) { - throw new Error(`GitHub API error ${res.status} for ${url}: ${await res.text()}`); - } - return res.json() as Promise; -} - -/** - * Compares two semver strings. Returns >0 if a>b, <0 if a v.replace(/^v/, "").split(".").map(n => parseInt(n, 10) || 0); - const pa = parse(a); - const pb = parse(b); - for (let i = 0; i < Math.max(pa.length, pb.length); i++) { - const diff = (pa[i] ?? 0) - (pb[i] ?? 0); - if (diff !== 0) return diff; - } - return 0; -} - -/** - * Finds the release tag immediately preceding `version` by fetching all tags and - * picking the highest one that is strictly lower than the target version. + * 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 findPreviousTag(version: string): Promise { - const target = version.replace(/^v/, ""); - const semverTag = /^v?\d+\.\d+\.\d+$/; +async function findMilestone(version: string): Promise { + const bare = version.replace(/^v/, ""); + const candidates = [bare, `v${bare}`]; - const tags: string[] = []; let page = 1; while (true) { - const batch = await fetchJson( - `${GITHUB_API}/repos/${GITHUB_REPO}/tags?per_page=100&page=${page}` + const milestones = await fetchJson( + `${GITHUB_API}/repos/${GITHUB_REPO}/milestones?state=all&per_page=100&page=${page}` ); - if (batch.length === 0) break; - tags.push(...batch.map(t => t.name)); - page++; - } - - const previous = tags - .filter(t => semverTag.test(t)) - .filter(t => compareSemver(t, target) < 0) - .sort(compareSemver) - .pop(); + if (milestones.length === 0) break; - if (!previous) { - throw new Error(`Could not determine the previous release tag for ${version}.`); + const found = milestones.find(m => candidates.includes(m.title)); + if (found) return found; + page++; } - return previous; -} - -/** - * Resolves the "head" ref to diff against `base`. Prefers the `vX.Y.Z` tag; if it - * does not exist yet (release not tagged), falls back to the `release/X.Y.Z` branch. - */ -async function resolveHeadRef(version: string): Promise { - const bare = version.replace(/^v/, ""); - const versionTag = `v${bare}`; - - const tag = await fetchJsonOrNull( - `${GITHUB_API}/repos/${GITHUB_REPO}/git/ref/tags/${versionTag}` - ); - if (tag) return versionTag; - - const releaseBranch = `release/${bare}`; - console.log( - ` Tag ${versionTag} not found — falling back to the ${releaseBranch} branch.` - ); - return releaseBranch; -} -/** - * Extracts PR numbers referenced as `(#1234)` in the commit messages between two refs. - */ -async function fetchPRNumbersInRange(base: string, head: string): Promise { - const compare = await fetchJson( - `${GITHUB_API}/repos/${GITHUB_REPO}/compare/${base}...${head}` - ); - - const numbers = new Set(); - for (const c of compare.commits) { - for (const match of c.commit.message.matchAll(/\(#(\d+)\)/g)) { - numbers.add(parseInt(match[1], 10)); - } - } - return [...numbers].sort((a, b) => a - b); + console.log("No milestone found."); + process.exit(0); } interface FetchResult { @@ -208,30 +123,36 @@ interface FetchResult { noChangelogNumbers: Set; } -async function fetchPRs(numbers: number[]): Promise { +async function fetchPRsForMilestone(milestoneNumber: number): Promise { const prs: PullRequest[] = []; const noChangelogNumbers = new Set(); + let page = 1; - for (const number of numbers) { - const pull = await fetchJsonOrNull( - `${GITHUB_API}/repos/${GITHUB_REPO}/pulls/${number}` + while (true) { + const issues = await fetchJson( + `${GITHUB_API}/repos/${GITHUB_REPO}/issues?milestone=${milestoneNumber}&state=closed&per_page=100&page=${page}` ); - // Referenced number that isn't actually a PR (e.g. an issue) — skip it. - if (!pull) continue; + if (issues.length === 0) break; - const hasNoChangelog = pull.labels.some(l => l.name === "no-changelog"); - if (hasNoChangelog) { - console.log(` Skipping PR #${pull.number} (no-changelog): ${pull.title}`); - noChangelogNumbers.add(pull.number); - continue; + for (const issue of issues) { + // The issues endpoint returns both issues and PRs; keep only PRs. + if (!issue.pull_request) continue; + + const hasNoChangelog = issue.labels.some(l => l.name === "no-changelog"); + if (hasNoChangelog) { + console.log(` Skipping PR #${issue.number} (no-changelog): ${issue.title}`); + noChangelogNumbers.add(issue.number); + continue; + } + prs.push({ + number: issue.number, + title: issue.title, + body: issue.body ?? "", + url: `https://github.com/${GITHUB_REPO}/pull/${issue.number}`, + author: issue.user?.login ?? "unknown" + }); } - prs.push({ - number: pull.number, - title: pull.title, - body: pull.body ?? "", - url: `https://github.com/${GITHUB_REPO}/pull/${pull.number}`, - author: pull.user?.login ?? "unknown" - }); + page++; } return { prs, noChangelogNumbers }; @@ -549,17 +470,12 @@ async function main(): Promise { console.log(`\nGenerating changelog for Webiny ${version}...`); - console.log(" Resolving release range on GitHub..."); - const [previousTag, headRef] = await Promise.all([ - findPreviousTag(version), - resolveHeadRef(version) - ]); - console.log(` Diffing ${previousTag}...${headRef} for referenced PRs...`); - const prNumbers = await fetchPRNumbersInRange(previousTag, headRef); - console.log(` Found ${prNumbers.length} referenced PR(s) in commit messages.`); - - console.log(" Fetching PR details..."); - const { prs, noChangelogNumbers } = await fetchPRs(prNumbers); + console.log(" Looking up milestone on GitHub..."); + const milestone = await findMilestone(version); + console.log(` Found milestone #${milestone.number}: "${milestone.title}"`); + + console.log(" Fetching merged PRs..."); + const { prs, noChangelogNumbers } = await fetchPRsForMilestone(milestone.number); console.log(` ${prs.length} pull request(s) eligible for the changelog.`); if (dryRun) { @@ -594,7 +510,7 @@ async function main(): Promise { } if (prs.length === 0) { - console.warn(" No pull requests found for this release range. Nothing to generate."); + console.warn(" No pull requests found for this milestone. Nothing to generate."); process.exit(0); }