diff --git a/.circleci/config.yml b/.circleci/config.yml index b0a6f7e48..1a38d4297 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,14 @@ version: 2.1 +# Reusable command definitions. +# See: https://circleci.com/docs/2.0/configuration-reference/#commands +commands: + install_cli_global: + steps: + - run: + name: install CLI globally + command: sudo npm install -g file:dist/openfn-cli.tgz + # Define a job to be invoked later in a workflow. # See: https://circleci.com/docs/2.0/configuration-reference/#jobs jobs: @@ -82,6 +91,21 @@ jobs: name: Type check command: pnpm test:types + build_openfnx: + docker: + - image: cimg/node:24.14 + resource_class: medium + steps: + - attach_workspace: + at: ~/project + - run: + name: Build local tarballs + command: pnpm pack:local dist --no-version + - persist_to_workspace: + root: ~/project + paths: + - dist + integration_test: docker: - image: cimg/node:<< parameters.node_version >> @@ -93,12 +117,7 @@ jobs: steps: - attach_workspace: at: ~/project - - run: - name: Build local tarballs - command: pnpm pack:local dist --no-version - - run: - name: install CLI globally - command: sudo npm install -g file:dist/openfn-cli.tgz + - install_cli_global - run: name: Check install command: openfn test --log info @@ -106,6 +125,21 @@ jobs: name: Run integration test suite command: pnpm test:integration + # This ensures that we can run a v1 CLI deploy in node 18, + # which is important for GH sync + legacy_test: + docker: + - image: cimg/node:18.20 + resource_class: medium + parallelism: 1 + steps: + - attach_workspace: + at: ~/project + - install_cli_global + - run: + name: Run legacy test suite + command: pnpm test:legacy + # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows workflows: @@ -128,9 +162,15 @@ workflows: - format: requires: - build + - build_openfnx: + requires: + - build - integration_test: matrix: parameters: node_version: ['22.20', '24.14.0'] requires: - - build + - build_openfnx + - legacy_test: + requires: + - build_openfnx diff --git a/integration-tests/legacy-deploy/.tool-versions b/integration-tests/legacy-deploy/.tool-versions new file mode 100644 index 000000000..f6efb75ce --- /dev/null +++ b/integration-tests/legacy-deploy/.tool-versions @@ -0,0 +1 @@ +nodejs 18.17.1 diff --git a/integration-tests/legacy-deploy/ava.config.js b/integration-tests/legacy-deploy/ava.config.js new file mode 100644 index 000000000..fd66585ce --- /dev/null +++ b/integration-tests/legacy-deploy/ava.config.js @@ -0,0 +1,3 @@ +export default { + files: ['test/**/*test.js'], +}; diff --git a/integration-tests/legacy-deploy/package.json b/integration-tests/legacy-deploy/package.json new file mode 100644 index 000000000..7eb35b66c --- /dev/null +++ b/integration-tests/legacy-deploy/package.json @@ -0,0 +1,28 @@ +{ + "name": "@openfn/integration-tests-legacy-deploy", + "private": true, + "version": "1.0.0", + "description": "CLI integration tests", + "author": "Open Function Group ", + "license": "ISC", + "type": "module", + "scripts": { + "clean": "rimraf dist repo", + "start": "docker run cli-integration-tests", + "test": "npx ava -s" + }, + "dependencies": { + "@types/node": "^18.19.130", + "ava": "5.3.1", + "date-fns": "^2.30.0", + "rimraf": "^6.1.3", + "tslib": "^2.8.1" + }, + "files": [ + "dist", + "README.md" + ], + "devDependencies": { + "@types/rimraf": "^3.0.2" + } +} diff --git a/integration-tests/legacy-deploy/test/deploy.test.js b/integration-tests/legacy-deploy/test/deploy.test.js new file mode 100644 index 000000000..514139885 --- /dev/null +++ b/integration-tests/legacy-deploy/test/deploy.test.js @@ -0,0 +1,109 @@ +import test from 'ava'; +import path from 'node:path'; +import fs from 'node:fs/promises'; +import http from 'node:http'; +import { exec } from 'node:child_process'; +import { rimraf } from 'rimraf'; + +const tmpDir = path.resolve('tmp/deploy'); + +let server; +let endpoint; +let lastDeploy; // the payload the CLI POSTs to the provisioning endpoint + +// A minimal stand-in for Lightning's provisioning API +const createMockServer = () => + new Promise((resolve) => { + const srv = http.createServer((req, res) => { + if (!req.url.startsWith('/api/provision')) { + res.statusCode = 404; + return res.end(); + } + + if (req.method === 'GET') { + // pretend no project exists yet, so everything is a "new" change + res.statusCode = 404; + return res.end(); + } + + let body = ''; + req.on('data', (chunk) => (body += chunk)); + req.on('end', () => { + lastDeploy = JSON.parse(body); + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ data: lastDeploy })); + }); + }); + srv.listen(0, () => resolve(srv)); + }); + +const run = (cmd) => + new Promise((resolve) => { + exec(cmd, (err, stdout, stderr) => { + resolve({ err, stdout, stderr }); + }); + }); + +const testProject = ` +name: test-project +workflows: + My-Workflow: + name: My Workflow + jobs: + my-job: + name: My Job + adaptor: '@openfn/language-common@latest' + body: 'fn(s => s)' + triggers: + webhook: + type: webhook + enabled: true + edges: + webhook->my-job: + condition_type: always + source_trigger: webhook + target_job: my-job +`.trim(); + +test.before(async () => { + server = await createMockServer(); + endpoint = `http://localhost:${server.address().port}`; + + process.env.IGNORE_DOT_ENV = 'true'; + process.env.OPENFN_ENDPOINT = endpoint; + process.env.OPENFN_API_KEY = 'test-key'; +}); + +test.after.always(() => { + server?.close(); +}); + +test.beforeEach(async () => { + await rimraf(tmpDir); + await fs.mkdir(tmpDir, { recursive: true }); + lastDeploy = undefined; +}); + +test.serial('deploy a local project', async (t) => { + // log the node version so CI confirms we're on the legacy runtime + t.log(process.version); + + await fs.writeFile(path.join(tmpDir, 'project.yaml'), testProject); + + const { stdout, stderr } = await run( + `openfn deploy \ + --project-path ${tmpDir}/project.yaml \ + --state-path ${tmpDir}/.state.json \ + --no-confirm \ + --log-json \ + -l debug` + ); + + t.falsy(stderr); + t.regex(stdout, /"message"\:\["Deployed"\]/); + + // the CLI should have posted our project to the mock endpoint + t.truthy(lastDeploy); + t.is(lastDeploy.name, 'test-project'); +}); diff --git a/package.json b/package.json index 72ffb6f04..cbb9c7766 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "pack": "pnpm -r run pack", "test:format": "prettier --check packages/*/src", "test:integration": "pnpm -r --filter=./integration-tests/* run test", + "test:legacy": "pnpm -C integration-tests/legacy-deploy run test", "test:types": "pnpm -r --filter=./packages/* run test:types", "test": "pnpm -r --filter=./packages/* run test", "_typesync": "pnpm exec typesync && pnpm -r exec typesync && pnpm install" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6c50d0ee..64b8abe06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,28 @@ importers: specifier: ^3.0.2 version: 3.0.2 + integration-tests/legacy-deploy: + dependencies: + '@types/node': + specifier: ^18.19.130 + version: 18.19.130 + ava: + specifier: 5.3.1 + version: 5.3.1 + date-fns: + specifier: ^2.30.0 + version: 2.30.0 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@types/rimraf': + specifier: ^3.0.2 + version: 3.0.2 + integration-tests/worker: dependencies: '@openfn/engine-multi': @@ -5433,7 +5455,7 @@ snapshots: '@slack/logger@4.0.1': dependencies: - '@types/node': 25.6.0 + '@types/node': 18.19.130 '@slack/types@2.21.1': {} @@ -5703,7 +5725,7 @@ snapshots: '@types/rimraf@3.0.2': dependencies: '@types/glob': 9.0.0 - '@types/node': 25.5.0 + '@types/node': 18.19.130 '@types/semver@7.5.8': {}