diff --git a/.changeset/chilly-shirts-bathe.md b/.changeset/chilly-shirts-bathe.md new file mode 100644 index 00000000000..cd17eb42e16 --- /dev/null +++ b/.changeset/chilly-shirts-bathe.md @@ -0,0 +1,5 @@ +--- +"create-fuels": patch +--- + +chore: add test to verify `create fuels` template integrity diff --git a/.env.example b/.env.example index fb61325cdd1..9db6730ab3f 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ FUEL_NETWORK_URL= TEST_WALLET_PVT_KEY= +PUBLISHED_NPM_VERSION= \ No newline at end of file diff --git a/.github/actions/pr-release/action.yaml b/.github/actions/pr-release/action.yaml new file mode 100644 index 00000000000..1e2bddb8e10 --- /dev/null +++ b/.github/actions/pr-release/action.yaml @@ -0,0 +1,41 @@ +name: "Publish PR to NPM" +inputs: + npm-token: + description: "NPM token for authenticating to NPM registry" + required: true + + github-token: + description: "GitHub token for authenticating to GitHub" + required: true + + pr-number: + description: "PR number" + default: ${{ github.event.pull_request.number }} + +outputs: + published_version: + description: "Published version of the PR" + value: ${{ steps.release.outputs.published_version }} + +runs: + using: "composite" + steps: + - name: Ensure NPM access + shell: bash + run: npm whoami + env: + NODE_AUTH_TOKEN: ${{ inputs.npm-token }} + + - name: Release to @pr-${{ inputs.pr-number }} tag on npm + id: release + shell: bash + run: | + pnpm changeset:next + git add .changeset/fuel-labs-ci.md + pnpm changeset version --snapshot pr-${{ inputs.pr-number }} + changetsets=$(pnpm changeset publish --tag pr-${{ inputs.pr-number }}) + published_version=$(echo "$changetsets" | grep -oP '@\K([0-9]+\.){2}[0-9]+-pr-${{ inputs.pr-number }}-\d+' | head -1) + echo "published_version=$published_version" >> $GITHUB_OUTPUT + env: + NODE_AUTH_TOKEN: ${{ inputs.npm-token }} + GITHUB_TOKEN: ${{ inputs.github-token }} diff --git a/.github/workflows/pr-release.yaml b/.github/workflows/pr-release.yaml index ce014ebeca4..6cc274cba00 100644 --- a/.github/workflows/pr-release.yaml +++ b/.github/workflows/pr-release.yaml @@ -20,31 +20,20 @@ jobs: - name: CI Setup uses: ./.github/actions/ci-setup - - name: Ensure NPM access - run: npm whoami - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Build run: pnpm build - - name: Release to @pr-${{ github.event.pull_request.number }} tag on npm + - name: Publish to NPM id: release - run: | - pnpm changeset:next - git add .changeset/fuel-labs-ci.md - pnpm changeset version --snapshot pr-${{ env.PR_NUMBER }} - changetsets=$(pnpm changeset publish --tag pr-${{ env.PR_NUMBER }}) - published_version=$(echo "$changetsets" | grep -oP '@\K([0-9]+\.){2}[0-9]+-pr-${{ env.PR_NUMBER }}-\d+' | head -1) - echo "published_version=$published_version" >> $GITHUB_OUTPUT - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} + uses: ./.github/actions/pr-release + with: + npm-token: ${{ secrets.NPM_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + pr-number: ${{ github.event.pull_request.number }} - uses: mshick/add-pr-comment@v2 with: message: | This PR is published in NPM with version **${{ steps.release.outputs.published_version }}** env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index db26ad596cc..018e15227d9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -58,9 +58,19 @@ jobs: with: fetch-depth: 0 - - name: CI Setup + - name: Test Setup uses: ./.github/actions/test-setup + - name: Publish PR to NPM + id: release + uses: ./.github/actions/pr-release + with: + npm-token: ${{ secrets.NPM_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Echo Published Version + run: echo ${{ steps.release.outputs.published_version }} + - name: Pretest run: pnpm pretest @@ -71,6 +81,7 @@ jobs: TEST_WALLET_PVT_KEY: ${{ secrets.TEST_WALLET_PVT_KEY }} TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }} FUEL_TESTNET_NETWORK_URL: ${{ secrets.FUEL_TESTNET_NETWORK_URL }} + PUBLISHED_NPM_VERSION: ${{ steps.release.outputs.published_version }} test: if: github.base_ref == 'master' || github.ref_name == 'master' diff --git a/packages/create-fuels/package.json b/packages/create-fuels/package.json index c995e8baac9..3a6df41e067 100644 --- a/packages/create-fuels/package.json +++ b/packages/create-fuels/package.json @@ -26,6 +26,7 @@ "prompts": "^2.4.2" }, "devDependencies": { + "@fuel-ts/errors": "workspace:*", "@fuel-ts/versions": "workspace:*", "@types/prompts": "^2.4.8", "glob": "^10.2.6" diff --git a/packages/create-fuels/test/cli.test.ts b/packages/create-fuels/test/cli.test.ts index 39ba46471d3..51d185dd276 100644 --- a/packages/create-fuels/test/cli.test.ts +++ b/packages/create-fuels/test/cli.test.ts @@ -1,20 +1,18 @@ import { mkdirSync } from 'fs'; -import { glob } from 'glob'; import type { ProgramsToInclude } from '../src/cli'; import { runScaffoldCli, setupProgram } from '../src/cli'; import type { ProjectPaths } from './utils/bootstrapProject'; -import { bootstrapProject, cleanupFilesystem, resetFilesystem } from './utils/bootstrapProject'; +import { + bootstrapProject, + cleanupFilesystem, + copyTemplate, + resetFilesystem, +} from './utils/bootstrapProject'; +import { generateArgv } from './utils/generateArgs'; import { mockLogger } from './utils/mockLogger'; - -const getAllFiles = async (pathToDir: string) => { - const files = await glob(`${pathToDir}/**/*`, { - ignore: ['**/node_modules/**', '**/.next/**', '**/sway-api/**'], - }); - const filesWithoutPrefix = files.map((file) => file.replace(pathToDir, '')); - return filesWithoutPrefix; -}; +import { filterOriginalTemplateFiles, getAllFiles } from './utils/templateFiles'; const possibleProgramsToInclude: ProgramsToInclude[] = [ { contract: true, predicate: false, script: false }, @@ -26,51 +24,6 @@ const possibleProgramsToInclude: ProgramsToInclude[] = [ { contract: true, predicate: true, script: true }, ]; -const defaultFlags = ['--pnpm']; - -const generateArgs = (programsToInclude: ProgramsToInclude, projectName?: string) => { - const args = ['', '']; - if (projectName) { - args.push(projectName); - } - if (programsToInclude.contract) { - args.push('-c'); - } - if (programsToInclude.predicate) { - args.push('-p'); - } - if (programsToInclude.script) { - args.push('-s'); - } - args.push(...defaultFlags); - return args; -}; - -const filterOriginalTemplateFiles = (files: string[], programsToInclude: ProgramsToInclude) => { - let newFiles = [...files]; - - newFiles = newFiles.filter((file) => { - if (file.includes('CHANGELOG')) { - return false; - } - if (!programsToInclude.contract && file.includes('contract')) { - return false; - } - if (!programsToInclude.predicate && file.includes('predicate')) { - return false; - } - if (!programsToInclude.script && file.includes('script')) { - return false; - } - if (['/gitignore', '/env'].includes(file)) { - return false; - } - return true; - }); - - return newFiles; -}; - /** * @group node */ @@ -80,6 +33,7 @@ describe('CLI', () => { beforeEach(() => { paths = bootstrapProject(__filename); + copyTemplate(paths.sourceTemplate, paths.template); }); afterEach(() => { @@ -95,7 +49,7 @@ describe('CLI', () => { test.each(possibleProgramsToInclude)( 'create-fuels extracts the template to the specified directory', async (programsToInclude) => { - const args = generateArgs(programsToInclude, paths.root); + const args = generateArgv(programsToInclude, paths.root); await runScaffoldCli({ program: setupProgram(), @@ -113,7 +67,7 @@ describe('CLI', () => { ); test('create-fuels reports an error if the project directory already exists', async () => { - const args = generateArgs( + const args = generateArgv( { contract: true, predicate: true, @@ -140,7 +94,7 @@ describe('CLI', () => { }); test('create-fuels reports an error if no programs are chosen to be included', async () => { - const args = generateArgs( + const args = generateArgv( { contract: false, predicate: false, diff --git a/packages/create-fuels/test/e2e.test.ts b/packages/create-fuels/test/e2e.test.ts new file mode 100644 index 00000000000..1d62730c74c --- /dev/null +++ b/packages/create-fuels/test/e2e.test.ts @@ -0,0 +1,65 @@ +import { safeExec } from '@fuel-ts/errors/test-utils'; +import { execSync } from 'child_process'; + +import type { ProjectPaths } from './utils/bootstrapProject'; +import { bootstrapProject, resetFilesystem } from './utils/bootstrapProject'; +import { generateArgs } from './utils/generateArgs'; +import { + filterForcBuildFiles, + filterOriginalTemplateFiles, + getAllFiles, +} from './utils/templateFiles'; + +const { log } = console; + +const PUBLISHED_NPM_VERSION = process.env.PUBLISHED_NPM_VERSION; +const programsToInclude = { contract: true, predicate: true, script: true }; +const availablePackages = ['pnpm']; + +/** + * @group e2e + */ +describe('`create fuels` package integrity', () => { + let paths: ProjectPaths; + let shouldSkip = false; + + beforeAll(() => { + if (!PUBLISHED_NPM_VERSION) { + log('Skipping live `create fuels` test'); + shouldSkip = true; + } + }); + + beforeEach(() => { + paths = bootstrapProject(__filename); + }); + + afterEach(() => { + resetFilesystem(paths.root); + }); + + it.each(availablePackages)( + 'should perform `%s create fuels`', + async (packageManager) => { + if (shouldSkip) { + return; + } + + const args = generateArgs(programsToInclude, paths.root, packageManager).join(' '); + const expectedTemplateFiles = await getAllFiles(paths.sourceTemplate).then((files) => + filterOriginalTemplateFiles(files, programsToInclude).filter(filterForcBuildFiles) + ); + + const { error: createFuelsError } = await safeExec(() => + execSync(`${packageManager} create fuels@${PUBLISHED_NPM_VERSION} ${args}`, { + stdio: 'inherit', + }) + ); + + const actualTemplateFiles = await getAllFiles(paths.root); + expect(createFuelsError).toBeUndefined(); + expect(actualTemplateFiles.sort()).toEqual(expectedTemplateFiles.sort()); + }, + { timeout: 30000 } + ); +}); diff --git a/packages/create-fuels/test/utils/bootstrapProject.ts b/packages/create-fuels/test/utils/bootstrapProject.ts index ba0828c6b3e..fb2983c98ef 100644 --- a/packages/create-fuels/test/utils/bootstrapProject.ts +++ b/packages/create-fuels/test/utils/bootstrapProject.ts @@ -4,6 +4,7 @@ import { basename, join } from 'path'; export type ProjectPaths = { root: string; template: string; + sourceTemplate: string; }; /** @@ -21,9 +22,6 @@ export const bootstrapProject = ( // Template paths const templateDir = join(templatesDir, templateName); const localTemplateDir = join(testTemplateDir, templateName); - if (!existsSync(localTemplateDir)) { - cpSync(templateDir, localTemplateDir, { recursive: true }); - } // Unique name const testFilename = basename(testFilepath.replace(/\./g, '-')); @@ -35,9 +33,16 @@ export const bootstrapProject = ( return { root, template: localTemplateDir, + sourceTemplate: templateDir, }; }; +export const copyTemplate = (srcDir: string, destDir: string) => { + if (!existsSync(destDir)) { + cpSync(srcDir, destDir, { recursive: true }); + } +}; + export const resetFilesystem = (dirPath: string) => { if (existsSync(dirPath)) { rmSync(dirPath, { recursive: true }); diff --git a/packages/create-fuels/test/utils/generateArgs.ts b/packages/create-fuels/test/utils/generateArgs.ts new file mode 100644 index 00000000000..80b93284f66 --- /dev/null +++ b/packages/create-fuels/test/utils/generateArgs.ts @@ -0,0 +1,28 @@ +import type { ProgramsToInclude } from '../../src/cli'; + +export const generateArgs = ( + programsToInclude: ProgramsToInclude, + projectName?: string, + packageManager: string = 'pnpm' +): string[] => { + const args = []; + if (projectName) { + args.push(projectName); + } + if (programsToInclude.contract) { + args.push('-c'); + } + if (programsToInclude.predicate) { + args.push('-p'); + } + if (programsToInclude.script) { + args.push('-s'); + } + args.push(`--${packageManager}`); + return args; +}; + +export const generateArgv = ( + programsToInclude: ProgramsToInclude, + projectName?: string +): string[] => ['', '', ...generateArgs(programsToInclude, projectName)]; diff --git a/packages/create-fuels/test/utils/templateFiles.ts b/packages/create-fuels/test/utils/templateFiles.ts new file mode 100644 index 00000000000..3121ec7b0fe --- /dev/null +++ b/packages/create-fuels/test/utils/templateFiles.ts @@ -0,0 +1,42 @@ +import { glob } from 'glob'; + +import type { ProgramsToInclude } from '../../src/cli'; + +export const getAllFiles = async (pathToDir: string) => { + const files = await glob(`${pathToDir}/**/*`, { + ignore: ['**/node_modules/**', '**/.next/**', '**/sway-api/**'], + }); + const filesWithoutPrefix = files.map((file) => file.replace(pathToDir, '')); + return filesWithoutPrefix; +}; + +export const filterOriginalTemplateFiles = ( + files: string[], + programsToInclude: ProgramsToInclude +) => { + let newFiles = [...files]; + + newFiles = newFiles.filter((file) => { + if (file.includes('CHANGELOG')) { + return false; + } + if (!programsToInclude.contract && file.includes('contract')) { + return false; + } + if (!programsToInclude.predicate && file.includes('predicate')) { + return false; + } + if (!programsToInclude.script && file.includes('script')) { + return false; + } + if (['/gitignore', '/env'].includes(file)) { + return false; + } + return true; + }); + + return newFiles; +}; + +export const filterForcBuildFiles = (file: string) => + !file.includes('contract/out') && !file.includes('script/out') && !file.includes('predicate/out'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05c8783c175..2f4c16d7f1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -803,6 +803,9 @@ importers: specifier: ^2.4.2 version: 2.4.2 devDependencies: + '@fuel-ts/errors': + specifier: workspace:* + version: link:../errors '@fuel-ts/versions': specifier: workspace:* version: link:../versions diff --git a/scripts/tests-find.sh b/scripts/tests-find.sh index 264d459b3cf..74e258411bb 100755 --- a/scripts/tests-find.sh +++ b/scripts/tests-find.sh @@ -12,4 +12,4 @@ elif [[ $* == *--browser* ]]; then grep -lE "@group\s+browser" $FILES elif [[ $* == *--e2e* ]]; then grep -lE "@group\s+e2e" $FILES -fi \ No newline at end of file +fi diff --git a/templates/nextjs/.gitignore b/templates/nextjs/.gitignore index bc516c4936d..22a95c4132a 100644 --- a/templates/nextjs/.gitignore +++ b/templates/nextjs/.gitignore @@ -31,7 +31,6 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts .fuels diff --git a/templates/nextjs/next-env.d.ts b/templates/nextjs/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/templates/nextjs/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information.