chore: 🚀 Migrate monorepo from Yarn to pnpm + Turbo integration + Configuration cleanup (#10165)

This PR modernizes the ZITADEL monorepo build system by migrating from
Yarn to pnpm, introducing Turbo for improved build orchestration, and
cleaning up configuration inconsistencies across all apps and packages.

### 🎯 Key Improvements

#### 📦 **Package Manager Migration (Yarn → pnpm)**
- **Performance**: Faster installs with pnpm's efficient symlink-based
node_modules structure
- **Disk space**: Significant reduction in disk usage through
content-addressable storage
- **Lockfile**: More reliable dependency resolution with pnpm-lock.yaml
- **Workspace support**: Better monorepo dependency management

####  **Turbo Integration**
- **Build orchestration**: Dependency-aware task execution across the
monorepo
- **Intelligent caching**: Dramatically faster builds on CI/CD and local
development
- **Parallel execution**: Optimal task scheduling based on dependency
graphs
- **Vercel optimization**: Enhanced build performance and caching on
Vercel deployments

#### 🧹 **Configuration Cleanup & Unification**
- **Removed config packages**: Eliminated `@zitadel/*-config` packages
and inlined configurations
- **Simplified dependencies**: Reduced complexity in package.json files
across all apps
- **Consistent tooling**: Unified prettier, ESLint, and TypeScript
configurations
- **Standalone support**: Improved prepare-standalone.js script for
subtree deployments

### 📋 Detailed Changes

#### **🔧 Build System & Dependencies**
-  Updated all package.json scripts to use `pnpm` instead of `yarn`
-  Replaced `yarn.lock` with pnpm-lock.yaml and regenerated
dependencies
-  Added Turbo configuration (turbo.json) to root and individual
packages
-  Configured proper dependency chains: `@zitadel/proto#generate` →
`@zitadel/client#build` → `console#build`
-  Added missing `@bufbuild/protobuf` dependency to console app for
TypeScript compilation

#### **🚀 CI/CD & Workflows**
-  Updated all GitHub Actions workflows to use `pnpm/action-setup@v4`
-  Migrated build processes to use Turbo with directory-based filters
(`--filter=./console`)
-  **New**: Added `docs.yml` workflow for building documentation
locally (helpful for contributors without Vercel access)
-  Fixed dependency resolution issues in lint workflows
-  Ensured proto generation always runs before builds and linting

#### **📚 Documentation & Proto Generation**
-  **Robust plugin management**: Enhanced plugin-download.sh with retry
logic and error handling
-  **Vercel compatibility**: Fixed protoc-gen-connect-openapi plugin
availability in Vercel builds
-  **API docs generation**: Resolved Docusaurus build errors with
OpenAPI plugin configuration
-  **Type safety**: Improved TypeScript type extraction patterns in
Angular components

#### **🛠️ Developer Experience**
-  Updated all README files to reference pnpm commands
-  Improved Makefile targets to use Turbo for consistent builds
-  Enhanced standalone build process for login app subtree deployments
-  Added debug utilities for troubleshooting build issues

#### **🗂️ File Structure & Cleanup**
-  Removed obsolete configuration packages and their references
-  Cleaned up Docker files to remove non-existent package copies
-  Updated workspace references and import paths
-  Streamlined turbo.json configurations across all packages

### 🎉 Benefits

1. ** Faster Builds**: Turbo's caching and parallel execution
significantly reduce build times
2. **🔄 Better Caching**: Improved cache hits on Vercel and CI/CD
environments
3. **🛠️ Simplified Maintenance**: Unified tooling and configuration
management
4. **📈 Developer Productivity**: Faster local development with optimized
dependency resolution
5. **🚀 Enhanced CI/CD**: More reliable and faster automated builds and
deployments
6. **📖 Better Documentation**: Comprehensive build documentation and
troubleshooting guides

### 🧪 Testing

-  All apps build successfully with new pnpm + Turbo setup
-  Proto generation works correctly across console, login, and docs
-  GitHub Actions workflows pass with new configuration
-  Vercel deployments work with enhanced plugin management
-  Local development workflow verified and documented

This migration sets a solid foundation for future development while
maintaining backward compatibility and improving the overall developer
experience.

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Max Peintner
2025-07-16 09:10:19 +02:00
committed by GitHub
parent 6d11145c77
commit 312b7b6010
152 changed files with 34249 additions and 37195 deletions

View File

@@ -139,7 +139,7 @@ export class FeaturesComponent {
}, {});
// to save special flags they have to be handled here
req.loginV2 = {
req['loginV2'] = {
required: toggleStates.loginV2.enabled,
baseUri: toggleStates.loginV2.baseUri,
};

View File

@@ -89,7 +89,7 @@ export class ActionTwoAddTargetDialogComponent {
nanos: 0,
};
const targetType: Extract<MessageInitShape<typeof CreateTargetRequestSchema>['targetType'], { case: TargetTypes }> =
const targetType: MessageInitShape<typeof CreateTargetRequestSchema>['targetType'] =
type === 'restWebhook'
? { case: type, value: { interruptOnError } }
: type === 'restCall'

View File

@@ -22,9 +22,8 @@ const CACHE_WARNING_MS = 5 * 60 * 1000; // 5 minutes
templateUrl: './oidc-webkeys.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OidcWebKeysComponent implements OnInit {
export class OidcWebKeysComponent {
protected readonly refresh = new Subject<true>();
protected readonly webKeysEnabled$: Observable<boolean>;
protected readonly webKeys$: Observable<WebKey[]>;
protected readonly inactiveWebKeys$: Observable<WebKey[]>;
protected readonly nextWebKeyCandidate$: Observable<WebKey | undefined>;
@@ -34,17 +33,12 @@ export class OidcWebKeysComponent implements OnInit {
constructor(
private readonly webKeysService: WebKeysService,
private readonly featureService: NewFeatureService,
private readonly toast: ToastService,
private readonly timestampToDatePipe: TimestampToDatePipe,
private readonly dialog: MatDialog,
private readonly destroyRef: DestroyRef,
private readonly router: Router,
private readonly route: ActivatedRoute,
) {
this.webKeysEnabled$ = this.getWebKeysEnabled().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const webKeys$ = this.getWebKeys(this.webKeysEnabled$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const webKeys$ = this.getWebKeys().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.webKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state !== State.INACTIVE)));
this.inactiveWebKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state === State.INACTIVE)));
@@ -52,34 +46,7 @@ export class OidcWebKeysComponent implements OnInit {
this.nextWebKeyCandidate$ = this.getNextWebKeyCandidate(this.webKeys$);
}
ngOnInit(): void {
// redirect away from this page if web keys are not enabled
// this also preloads the web keys enabled state
this.webKeysEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (webKeysEnabled) => {
if (webKeysEnabled) {
return;
}
await this.router.navigate([], {
relativeTo: this.route,
queryParamsHandling: 'merge',
queryParams: {
id: null,
},
});
});
}
private getWebKeysEnabled() {
return defer(() => this.featureService.getInstanceFeatures()).pipe(
map((features) => features.webKey?.enabled ?? false),
catchError((err) => {
this.toast.showError(err);
return of(false);
}),
);
}
private getWebKeys(webKeysEnabled$: Observable<boolean>) {
private getWebKeys() {
return this.refresh.pipe(
startWith(true),
switchMap(() => {
@@ -87,12 +54,6 @@ export class OidcWebKeysComponent implements OnInit {
}),
map(({ webKeys }) => webKeys),
catchError(async (err) => {
const webKeysEnabled = await firstValueFrom(webKeysEnabled$);
// suppress errors if web keys are not enabled
if (!webKeysEnabled) {
return [];
}
this.toast.showError(err);
return [];
}),

View File

@@ -204,7 +204,7 @@ export class UserCreateV2Component implements OnInit {
if (authenticationFactor.factor === 'initialPassword') {
const { password } = authenticationFactor.form.getRawValue();
humanReq.passwordType = {
humanReq['passwordType'] = {
case: 'password',
value: {
password,

View File

@@ -8,12 +8,14 @@ import { Gender, HumanProfile, HumanProfileSchema } from '@zitadel/proto/zitadel
import { filter, startWith } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Profile } from '@zitadel/proto/zitadel/user_pb';
import { create } from '@bufbuild/protobuf';
//@ts-ignore
import { create } from '@zitadel/client';
function toHumanProfile(profile: HumanProfile | Profile): HumanProfile {
if (profile.$typeName === 'zitadel.user.v2.HumanProfile') {
return profile;
}
return create(HumanProfileSchema, {
givenName: profile.firstName,
familyName: profile.lastName,

View File

@@ -36,10 +36,10 @@ import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb';
type Query = Exclude<
Exclude<MessageInitShape<typeof ListUsersRequestSchema>['queries'], undefined>[number]['query'],
undefined
>;
type ListUsersRequest = MessageInitShape<typeof ListUsersRequestSchema>;
type QueriesArray = NonNullable<ListUsersRequest['queries']>;
type QueryWrapper = QueriesArray extends readonly (infer T)[] ? T : never;
type Query = NonNullable<QueryWrapper extends { query?: infer Q } ? Q : never>;
@Component({
selector: 'cnsl-user-table',

View File

@@ -229,94 +229,3 @@ export class UserService {
return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req));
}
}
function userToV2(user: User): UserV2 {
const details = user.getDetails();
return create(UserSchema, {
userId: user.getId(),
details: details && detailsToV2(details),
state: user.getState() as number as UserState,
username: user.getUserName(),
loginNames: user.getLoginNamesList(),
preferredLoginName: user.getPreferredLoginName(),
type: typeToV2(user),
});
}
function detailsToV2(details: ObjectDetails): Details {
const changeDate = details.getChangeDate();
return create(DetailsSchema, {
sequence: BigInt(details.getSequence()),
changeDate: changeDate && timestampToV2(changeDate),
resourceOwner: details.getResourceOwner(),
});
}
function timestampToV2(timestamp: Timestamp): TimestampV2 {
return create(TimestampSchema, {
seconds: BigInt(timestamp.getSeconds()),
nanos: timestamp.getNanos(),
});
}
function typeToV2(user: User): UserV2['type'] {
const human = user.getHuman();
if (human) {
return { case: 'human', value: humanToV2(user, human) };
}
const machine = user.getMachine();
if (machine) {
return { case: 'machine', value: machineToV2(machine) };
}
return { case: undefined };
}
function humanToV2(user: User, human: Human): HumanUser {
const profile = human.getProfile();
const email = human.getEmail()?.getEmail();
const phone = human.getPhone();
const passwordChanged = human.getPasswordChanged();
return create(HumanUserSchema, {
userId: user.getId(),
state: user.getState() as number as UserState,
username: user.getUserName(),
loginNames: user.getLoginNamesList(),
preferredLoginName: user.getPreferredLoginName(),
profile: profile && humanProfileToV2(profile),
email: { email },
phone: phone && humanPhoneToV2(phone),
passwordChangeRequired: false,
passwordChanged: passwordChanged && timestampToV2(passwordChanged),
});
}
function humanProfileToV2(profile: Profile): HumanProfile {
return create(HumanProfileSchema, {
givenName: profile.getFirstName(),
familyName: profile.getLastName(),
nickName: profile.getNickName(),
displayName: profile.getDisplayName(),
preferredLanguage: profile.getPreferredLanguage(),
gender: profile.getGender() as number as Gender,
avatarUrl: profile.getAvatarUrl(),
});
}
function humanPhoneToV2(phone: Phone): HumanPhone {
return create(HumanPhoneSchema, {
phone: phone.getPhone(),
isVerified: phone.getIsPhoneVerified(),
});
}
function machineToV2(machine: Machine): MachineUser {
return create(MachineUserSchema, {
name: machine.getName(),
description: machine.getDescription(),
hasSecret: machine.getHasSecret(),
accessTokenType: machine.getAccessTokenType() as number as AccessTokenType,
});
}