Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions e2e/harmony/ci-sync.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,152 @@ describe('bit ci sync', function () {
});
});

// A real scope carries components with tag blockers (e.g. circular dependencies). The snap
// covers only the lane's pending components. A blocker on an untouched component must not halt it.
describe('a snap-blocking issue on a component the lane never touches', () => {
const LANE = 'clean-lane';
let defaultBranch: string;
let devPath: string;

before(() => {
({ defaultBranch } = setupSyncWorkspace({ lanes: ['*'] }));
// a circular pair on main: a tag blocker on components no lane sync will ever snap
helper.fs.outputFile('comp3/index.js', `require('@${helper.scopes.remote}/comp4');`);
helper.fs.outputFile('comp4/index.js', `require('@${helper.scopes.remote}/comp3');`);
helper.command.addComponent('comp3');
helper.command.addComponent('comp4');
helper.command.install();
helper.command.tagAllWithoutBuild('--ignore-issues="CircularDependencies"');
helper.command.export();
helper.command.runCmd('git add -A');
helper.command.runCmd('git commit -m "add a circular pair to main"');
helper.command.runCmd(`git push origin ${defaultBranch}`);
devPath = createLaneWithSnap(LANE, { 'comp1/index.js': comp1Src('lane-snap-1') }, 'lane snap 1');
seedSync(LANE);
branchSideCommit(LANE, defaultBranch, 'comp1/index.js', comp1Src('dev-commit-1'), 'dev commit on comp1');
});

it('snaps the dev commit onto the lane although the untouched pair has a tag blocker', () => {
const { output, exitCode } = syncRun(LANE);
expect(exitCode, `bit ci sync output:\n${output}`).to.equal(0);
expect(output).to.not.include('Workspace status verification failed');
expect(output).to.include(`${LANE} -> export-branch`);
expect(laneTipFile(devPath, 'comp1/index.js')).to.include('dev-commit-1');
});
});

// This reproduces an engine bump with one bit binary: a committed root policy moves a recorded
// package range. The lane run snaps only the git-authored change, and reports the drifted
// component instead of sweeping it into the dev's snap.
describe('dependency-context drift is excluded from the lane snap', () => {
const LANE = 'drift-lane';
let defaultBranch: string;
let devPath: string;

before(() => {
({ defaultBranch } = setupSyncWorkspace({ lanes: ['*'] }));
helper.fs.outputFile('comp2/index.js', `require('is-odd');\nmodule.exports = () => 'comp2: with-pkg';\n`);
helper.workspaceJsonc.addPolicyToDependencyResolver({ dependencies: { 'is-odd': '1.0.0' } });
helper.command.install();
helper.command.tagAllWithoutBuild();
helper.command.export();
helper.command.runCmd('git add -A');
helper.command.runCmd('git commit -m "comp2 records is-odd 1.0.0"');
helper.command.runCmd(`git push origin ${defaultBranch}`);
// Create the lane while the policy still matches comp2's recorded range. Otherwise the dev's
// own (unscoped) `bit snap` sweeps the drift in too, and leaves nothing for `bit ci sync` to
// exclude.
devPath = createLaneWithSnap(LANE, { 'comp1/index.js': comp1Src('lane-snap-1') }, 'lane snap 1');
seedSync(LANE);
branchSideCommit(LANE, defaultBranch, 'comp1/index.js', comp1Src('dev-commit-1'), 'dev commit on comp1');
// The default branch's resolution context moves after the lane forks — the engine-bump
// analogue: `bit ci sync` boots on the default branch and resolves the workspace policy/engine
// aggregate once, at boot. Mid-run branch checkouts do not re-read it (`Workspace._reloadConsumer`
// reloads the consumer and bitmap, not this). The run's resolution context is fixed at boot,
// regardless of which branch it later checks out — the same drift a real engine bump produces
// on an untouched component.
helper.workspaceJsonc.addPolicyToDependencyResolver({ dependencies: { 'is-odd': '3.0.1' } });
helper.command.install();
helper.command.runCmd('git add -A');
helper.command.runCmd('git commit -m "bump is-odd policy (engine-bump analogue)"');
helper.command.runCmd(`git push origin ${defaultBranch}`);
});

it('snaps the dev commit, reports the drifted component, and keeps it off the lane', () => {
const before = remoteLaneFingerprint(LANE);
expect(before).to.not.include('comp2');
const { output, exitCode } = syncRun(LANE);
expect(exitCode, `bit ci sync output:\n${output}`).to.equal(0);
expect(output).to.include('dependency-context drift');
expect(output).to.include('comp2');
expect(laneTipFile(devPath, 'comp1/index.js')).to.include('dev-commit-1');
expect(remoteLaneFingerprint(LANE)).to.not.include('comp2');
});
});

// Convergence on main consumes the drift: one patch tag, exported, with the .bitmap bump riding
// the bit-sync/main flow. The circular pair also drifts. The tag must tolerate the blocker
// already present on the recorded heads (tagged with --ignore-issues originally).
describe('main reconcile converges dependency-context drift', () => {
const SYNC_BRANCH = 'bit-sync/main';
let defaultBranch: string;

before(() => {
({ defaultBranch } = setupSyncWorkspace({ lanes: ['*'] }));
helper.fs.outputFile('comp2/index.js', `require('is-odd');\nmodule.exports = () => 'comp2: with-pkg';\n`);
helper.fs.outputFile('comp3/index.js', `require('is-odd');\nrequire('@${helper.scopes.remote}/comp4');`);
helper.fs.outputFile('comp4/index.js', `require('@${helper.scopes.remote}/comp3');`);
helper.command.addComponent('comp3');
helper.command.addComponent('comp4');
helper.workspaceJsonc.addPolicyToDependencyResolver({ dependencies: { 'is-odd': '1.0.0' } });
helper.command.install();
helper.command.tagAllWithoutBuild('--ignore-issues="CircularDependencies"');
helper.command.export();
helper.command.runCmd('git add -A');
helper.command.runCmd('git commit -m "record deps under is-odd 1.0.0"');
helper.command.runCmd(`git push origin ${defaultBranch}`);
helper.workspaceJsonc.addPolicyToDependencyResolver({ dependencies: { 'is-odd': '3.0.1' } });
// A bare workspace.jsonc edit is invisible to a running process. Only a real `install()`
// re-run moves what gets resolved from disk (node_modules/lockfile).
helper.command.install();
helper.command.runCmd('git add -A');
helper.command.runCmd('git commit -m "bump is-odd policy"');
helper.command.runCmd(`git push origin ${defaultBranch}`);
});

it('dry-run reports the convergence and tags nothing', () => {
const { output, exitCode } = syncRun('--main --dry-run');
expect(exitCode, output).to.equal(0);
expect(output).to.include('dependency-context drift');
expect(output).to.include('dry-run');
// Pin the returned summary line, not just the mid-run log — that would pass either way.
// The count is left out; it is not the stable part.
expect(output).to.include('main -> dry-run: would converge');
const list = helper.command.listRemoteScopeParsed();
const comp2 = list.find((c: any) => c.id.includes('comp2'));
// comp2 is already recorded at 0.0.2 from the setup's own tag (is-odd 1.0.0). The dry-run
// must not advance it further; it need not leave it below 0.0.2.
expect(comp2.localVersion || comp2.currentVersion).to.equal('0.0.2');
});

it('converges: one patch tag with the alignment message, exported, .bitmap bump on the sync branch', () => {
const { output, exitCode } = syncRun('--main');
expect(exitCode, output).to.equal(0);
expect(output).to.include('align dependency context');
expect(output).to.include('main -> pushed sync commit to');
// Checks comp2's convergence bump (0.0.2 -> 0.0.3). 0.0.2 alone is already true at the fork
// point, so it would pass regardless of convergence.
expect(fileOnBranch(SYNC_BRANCH, '.bitmap')).to.include('0.0.3');
});

it('the next run finds a converged pair and no-ops', () => {
const { output, exitCode } = syncRun('--main');
expect(exitCode, output).to.equal(0);
expect(output).to.match(/converged/i);
expect(output).to.not.include('align dependency context');
});
});

describe('a stale bit-sync/main that conflicts with the default branch', () => {
const SYNC_BRANCH = 'bit-sync/main';
let defaultBranch: string;
Expand Down
17 changes: 17 additions & 0 deletions scopes/git/ci/ci.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,23 @@ pull-request diff, and a person rejects it: close the pull request. With `mainSy
command commits the same drift on the default branch, and uses no sync branch and no pull request. The push
is a plain push, so the run stops if the default branch moved during the run.

### Dependency-context drift

A mirror workspace can show modified components when git has no changes. The
cause is a moved resolution context: the pinned bit engine ships new env
dependency templates, or a committed root policy changes a recorded range.
This is a real dependency change that the repository introduces.

`bit ci sync` consumes it. A main run tags the drifted components with a
patch bump and a message of the shape
`chore: align dependency context (recorded with bit X, workspace runs bit Y)`,
then exports. A lane run snaps only the components with git-authored changes
and reports the drift; it does not snap a drifted component directly, but a
drifted component that depends on a snapped component is auto-snapped as
its dependent. Pin the engine in `workspace.jsonc`
(`"teambit.harmony/bit": { "engine": "<version>" }`) so the context moves only
when a commit moves it.

### Git host providers and credentials

The command uses plain git for every git operation. The command uses a `GitHostProvider` for every
Expand Down
Loading