diff --git a/packages/@o3r/pipeline/schematics/ng-add/index.spec.ts b/packages/@o3r/pipeline/schematics/ng-add/index.spec.ts index d68ec03008..71c0b61b69 100644 --- a/packages/@o3r/pipeline/schematics/ng-add/index.spec.ts +++ b/packages/@o3r/pipeline/schematics/ng-add/index.spec.ts @@ -65,3 +65,71 @@ describe('ng-add', () => { }); }); + +describe('ng-add with yarn should generate a GitHub workflow', () => { + + let initialTree: Tree; + + beforeEach(() => { + initialTree = Tree.empty(); + initialTree.create('angular.json', fs.readFileSync(path.resolve(__dirname, '..', '..', 'testing', 'mocks', 'angular.mocks.yarn.json'))); + }); + + it('when no yarnrc.yml', async () => { + const runner = new SchematicTestRunner('@o3r/pipeline', collectionPath); + const tree = await runner.runSchematic('ng-add', { + toolchain: 'github' + } as NgAddSchematicsSchema, initialTree); + + expect(tree.exists('.github/actions/setup/action.yml')).toBe(true); + expect(tree.exists('.github/workflows/main.yml')).toBe(true); + expect(tree.exists('.npmrc')).toBe(false); + + expect(tree.readText('.github/actions/setup/action.yml')).toContain('(yarn cache dir)'); + + }); + + it('with yarnrc.yml without yarnPath', async () => { + const runner = new SchematicTestRunner('@o3r/pipeline', collectionPath); + initialTree.create('.yarnrc.yml', ''); + const tree = await runner.runSchematic('ng-add', { + toolchain: 'github' + } as NgAddSchematicsSchema, initialTree); + + expect(tree.exists('.github/actions/setup/action.yml')).toBe(true); + expect(tree.exists('.github/workflows/main.yml')).toBe(true); + expect(tree.exists('.npmrc')).toBe(false); + + expect(tree.readText('.github/actions/setup/action.yml')).toContain('(yarn config get cacheFolder)'); + }); + + it('with yarnrc.yml with yarnPath v1', async () => { + const runner = new SchematicTestRunner('@o3r/pipeline', collectionPath); + initialTree.create('.yarnrc.yml', 'yarnPath: .yarn/releases/yarn-1.2.3.cjs'); + const tree = await runner.runSchematic('ng-add', { + toolchain: 'github' + } as NgAddSchematicsSchema, initialTree); + + expect(tree.exists('.github/actions/setup/action.yml')).toBe(true); + expect(tree.exists('.github/workflows/main.yml')).toBe(true); + expect(tree.exists('.npmrc')).toBe(false); + + expect(tree.readText('.github/actions/setup/action.yml')).toContain('(yarn cache dir)'); + expect(tree.readText('.github/actions/setup/action.yml')).not.toContain('.yarn/unplugged'); + }); + + it('with yarnrc.yml with yarnPath v2', async () => { + const runner = new SchematicTestRunner('@o3r/pipeline', collectionPath); + initialTree.create('.yarnrc.yml', 'yarnPath: .yarn/releases/yarn-3.2.1.cjs'); + const tree = await runner.runSchematic('ng-add', { + toolchain: 'github' + } as NgAddSchematicsSchema, initialTree); + + expect(tree.exists('.github/actions/setup/action.yml')).toBe(true); + expect(tree.exists('.github/workflows/main.yml')).toBe(true); + expect(tree.exists('.npmrc')).toBe(false); + + expect(tree.readText('.github/actions/setup/action.yml')).toContain('(yarn config get cacheFolder)'); + expect(tree.readText('.github/actions/setup/action.yml')).toContain('.yarn/unplugged'); + }); +}); diff --git a/packages/@o3r/pipeline/schematics/ng-add/index.ts b/packages/@o3r/pipeline/schematics/ng-add/index.ts index 5afa9fca0a..4e809328f0 100644 --- a/packages/@o3r/pipeline/schematics/ng-add/index.ts +++ b/packages/@o3r/pipeline/schematics/ng-add/index.ts @@ -1,9 +1,22 @@ -import { apply, chain, MergeStrategy, mergeWith, move, Rule, template, url } from '@angular-devkit/schematics'; -import type { NgAddSchematicsSchema } from './schema'; -import {dump, load} from 'js-yaml'; -import * as path from 'node:path'; +import { apply, chain, MergeStrategy, mergeWith, move, Rule, template, type Tree, url } from '@angular-devkit/schematics'; +import { dump, load } from 'js-yaml'; import * as fs from 'node:fs'; +import * as path from 'node:path'; import type { PackageJson } from 'type-fest'; +import type { NgAddSchematicsSchema } from './schema'; + +/** + * Determines if the Yarn version is 2 or higher based on the contents of the .yarnrc.yml file. + * @param tree tree + */ +function isYarn2(tree: Tree) { + const yarnrcPath = '/.yarnrc.yml'; + if (tree.exists(yarnrcPath)) { + const { yarnPath } = (load(tree.readText(yarnrcPath)) || {}) as { yarnPath?: string }; + return !yarnPath || !/yarn-1\./.test(yarnPath); + } + return false; +} /** * Add an Otter CI pipeline to an Angular Project @@ -26,12 +39,14 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { } context.logger.info(`Setting up pipeline for package manager: "${packageManager}" `); const setupCommand = packageManager === 'yarn' ? 'yarn install --immutable' : 'npm ci'; + const yarn2 = packageManager === 'yarn' && isYarn2(tree); const baseTemplateSource = apply(url(`./templates/${options.toolchain}`), [ template({ ...options, packageManager, setupCommand, actionVersionString, + yarn2, dot: '.' }), move(tree.root.path) @@ -41,16 +56,13 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { if (!options.npmRegistry) { return tree; } - if (packageManager === 'yarn') { + if (yarn2) { const yarnrcPath = '/.yarnrc.yml'; - if (!tree.exists(yarnrcPath)) { - tree.create(yarnrcPath, dump({'npmRegistryServer': options.npmRegistry}, {indent: 2})); - } else { - const yarnrcContent = load(tree.readText(yarnrcPath)) as any; - yarnrcContent.npmRegistryServer = options.npmRegistry; - tree.overwrite(yarnrcPath, dump(yarnrcContent, {indent: 2})); - } - } else if (packageManager === 'npm') { + const yarnrcContent = load(tree.readText(yarnrcPath)) as { npmRegistryServer?: string }; + yarnrcContent.npmRegistryServer = options.npmRegistry; + tree.overwrite(yarnrcPath, dump(yarnrcContent, {indent: 2})); + } else { + // both npm and yarn 1 use .npmrc for the registry const npmrcPath = '/.npmrc'; if (!tree.exists(npmrcPath)) { tree.create(npmrcPath, `registry=${options.npmRegistry}`); diff --git a/packages/@o3r/pipeline/schematics/ng-add/templates/github/__dot__github/actions/setup/action.yml b/packages/@o3r/pipeline/schematics/ng-add/templates/github/__dot__github/actions/setup/action.yml index 2f6c6e6853..704e8b45e2 100644 --- a/packages/@o3r/pipeline/schematics/ng-add/templates/github/__dot__github/actions/setup/action.yml +++ b/packages/@o3r/pipeline/schematics/ng-add/templates/github/__dot__github/actions/setup/action.yml @@ -6,16 +6,30 @@ runs: steps: - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 with: - node-version: 20 - cache: <%= packageManager %> + node-version: 20<% if (packageManager !== 'yarn') { %> + cache: <%= packageManager %><% } %> - name: Enable Corepack shell: bash - run: corepack enable - - name: Install -<% if (npmRegistry) { %> + run: corepack enable<% if (packageManager === 'yarn') { %> + - name: Get yarn cache directory path + shell: bash + id: yarn-cache-dir-path + run: echo "dir=$(<% if (yarn2) { %>yarn config get cacheFolder<% } else { %>yarn cache dir<% } %>)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + <% if (yarn2) { %>.yarn/unplugged + .pnp.cjs + .pnp.loader.mjs<% } %> + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn + ${{ runner.os }}<% } %> + - name: Install<% if (npmRegistry) { %> env: COREPACK_NPM_REGISTRY: <%= npmRegistry %> - COREPACK_INTEGRITY_KEYS: "" -<% } %> + COREPACK_INTEGRITY_KEYS: ""<% } %> shell: bash run: <%= setupCommand %> diff --git a/packages/@o3r/pipeline/testing/mocks/angular.mocks.yarn.json b/packages/@o3r/pipeline/testing/mocks/angular.mocks.yarn.json new file mode 100644 index 0000000000..326821bc35 --- /dev/null +++ b/packages/@o3r/pipeline/testing/mocks/angular.mocks.yarn.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular/cli/lib/config/workspace-schema.json", + "version": 1, + "newProjectRoot": ".", + "cli": { + "packageManager": "yarn" + }, + "projects": { + "test-project": { + "projectType": "application", + "root": ".", + "sourceRoot": "./src", + "prefix": "tst", + "architect": { + + } + } + } +}