From 7c8d35eabdad42cb3fd16c09473961ab6b565dba Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 7 Oct 2026 18:02:48 +0100 Subject: [PATCH] ignore checkout meta --- packages/cli/src/projects/checkout.ts | 7 ++- packages/cli/src/projects/util.ts | 16 +++++++ packages/cli/test/projects/checkout.test.ts | 9 ++++ packages/cli/test/projects/util.test.ts | 50 +++++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index c22a7b375..d004773cf 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -11,7 +11,11 @@ import * as o from '../options'; import * as po from './options'; import type { Opts } from './options'; -import { tidyWorkflowDir, updateForkedFrom } from './util'; +import { + ensureCheckoutIgnored, + tidyWorkflowDir, + updateForkedFrom, +} from './util'; import { createProjectCredentials } from './credentials-helpers'; import abort from '../util/abort'; @@ -167,6 +171,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { delete files[switchProject.generateConfig().path]; delete files[switchProject.generateCheckout(options.branch).path]; } + await ensureCheckoutIgnored(workspacePath); for (const f in files) { if (files[f]) { fs.mkdirSync(path.join(workspacePath, path.dirname(f)), { diff --git a/packages/cli/src/projects/util.ts b/packages/cli/src/projects/util.ts index a34e8d7d2..b77d7dd97 100644 --- a/packages/cli/src/projects/util.ts +++ b/packages/cli/src/projects/util.ts @@ -260,6 +260,21 @@ export const updateForkedFrom = (proj: Project) => { return proj; }; +// The checkout state in .openfn is local to each machine, so it shouldn't be +// committed. Make git ignore everything in there. +// Set OPENFN_IGNORE_CHECKOUT_META=true to skip this +export const ensureCheckoutIgnored = async (workspacePath: string) => { + if (process.env.OPENFN_IGNORE_CHECKOUT_META === 'true') { + return; + } + + const filePath = path.resolve(workspacePath, '.openfn', '.gitignore'); + if (!fs.existsSync(filePath)) { + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, '*\n'); + } +}; + // Set up the checkout file for a project without touching anything else // (use this when the workflows are already on disk, like after a git clone) export const writeCheckoutFile = async ( @@ -268,6 +283,7 @@ export const writeCheckoutFile = async ( branch?: string | false | null ) => { updateForkedFrom(project); + await ensureCheckoutIgnored(workspacePath); const { path: checkoutPath, content } = project.generateCheckout(branch); const filePath = path.resolve(workspacePath, checkoutPath); diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index d4762ec1f..fb6d69e7a 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -222,6 +222,15 @@ test.serial('checkout: same id as active', async (t) => { ); }); +test.serial('checkout: makes git ignore the checkout state', async (t) => { + await checkoutHandler( + { command: 'project-checkout', project: 'my-project', workspace: '/ws' }, + logger + ); + + t.is(fs.readFileSync('/ws/.openfn/.gitignore', 'utf8'), '*\n'); +}); + test.serial( 'checkout: writes forked_from based on version history', async (t) => { diff --git a/packages/cli/test/projects/util.test.ts b/packages/cli/test/projects/util.test.ts index b5a7fb44e..3f5b36ce0 100644 --- a/packages/cli/test/projects/util.test.ts +++ b/packages/cli/test/projects/util.test.ts @@ -6,6 +6,7 @@ import { findLocallyChangedWorkflows, tidyWorkflowDir, writeCheckoutFile, + ensureCheckoutIgnored, } from '../../src/projects/util'; test.afterEach(() => { @@ -340,3 +341,52 @@ test.serial( t.deepEqual(fs.readdirSync('/ws/workflows/wf'), ['wf.yaml']); } ); + +test.serial( + 'ensureCheckoutIgnored: makes git ignore everything in .openfn', + async (t) => { + mock({ '/ws': {} }); + + await ensureCheckoutIgnored('/ws'); + + t.is(fs.readFileSync('/ws/.openfn/.gitignore', 'utf8'), '*\n'); + } +); + +test.serial( + 'ensureCheckoutIgnored: leaves an existing .gitignore alone', + async (t) => { + mock({ '/ws/.openfn/.gitignore': 'custom\n' }); + + await ensureCheckoutIgnored('/ws'); + + t.is(fs.readFileSync('/ws/.openfn/.gitignore', 'utf8'), 'custom\n'); + } +); + +test.serial( + 'ensureCheckoutIgnored: does nothing if OPENFN_IGNORE_CHECKOUT_META is set', + async (t) => { + mock({ '/ws': {} }); + process.env.OPENFN_IGNORE_CHECKOUT_META = 'true'; + + try { + await ensureCheckoutIgnored('/ws'); + } finally { + delete process.env.OPENFN_IGNORE_CHECKOUT_META; + } + + t.false(fs.existsSync('/ws/.openfn')); + } +); + +test.serial( + 'writeCheckoutFile: makes git ignore the checkout state', + async (t) => { + mock({ '/ws': {} }); + + await writeCheckoutFile('/ws', trackedProject()); + + t.is(fs.readFileSync('/ws/.openfn/.gitignore', 'utf8'), '*\n'); + } +);