feat: Support skipping Vercel's CI (#588)

* fix: filtering gh bot comments

* cr

* cr

* test empty commit

* [skip ci]

* [skip deploy]

* feat: Support skipping Vercel's CI

* push empty commit at the end

* cr

* testing [skip ci]

* testing [skip deploy]

* testing [skip ci]

* testing [skip ci]

* testing [skip ci]

* fix command

* cr

* cr

* cr

* cr
This commit is contained in:
Brace Sproul 2025-07-29 11:13:55 -07:00 • committed by GitHub
parent fb705d367a
commit 2f2e543f8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 162 additions and 2 deletions

View file

@ -27,7 +27,8 @@
"setup/intro",
"setup/development",
"setup/authentication",
"setup/monorepo"
"setup/monorepo",
"setup/ci"
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

69
apps/docs/setup/ci.mdx Normal file
View file

@ -0,0 +1,69 @@
---
title: "CI Configuration"
description: "How to configure your CI pipeline for Open SWE"
---
## Skip CI until last commit
Open SWE will push a commit after every change to the repository. This will cause your GitHub CI workflows to run for every commit, which is unnecessary.
To prevent this, Open SWE supports adding an environment variable which will append `[skip ci]` to the commit message. This can be used to make GitHub skip CI for that commit.
Below are instructions showing how to use this to skip creating Vercel CI preview deployments on every commit:
<Steps>
<Step title="Set the environment variable">
Set the environment variable `SKIP_CI_UNTIL_LAST_COMMIT` to `true` in Open SWE's environment variables:
`apps/open-swe/.env`
```bash
SKIP_CI_UNTIL_LAST_COMMIT="true"
```
</Step>
<Step title="Add custom 'Ignored Build Step' in Vercel">
Add a custom 'Ignored Build Step' in Vercel to skip CI when commit messages contain the string `[skip ci]`.
1. Navigate to your Vercel project dashboard
2. Go to **Settings** → **Git** tab
3. Scroll to **Ignored Build Step** section
4. Select **Custom** and add the following script:
![Vercel Custom Build Step Screenshot](/images/vercel_ignored_build_script_screenshot.png)
```bash
git log -1 --pretty=oneline --abbrev-commit | grep -w "\[skip ci\]" && exit 0 || exit 1
```
<Callout type="warning">
**Do not test this command locally** as it will close your terminal. Use the testing method below instead.
</Callout>
</Step>
<Step title="Test the configuration (optional)">
To verify your setup works without closing your terminal, use this safe testing command:
```bash
# Safe local testing - won't close your terminal
if git log -1 --pretty=oneline --abbrev-commit | grep -w "\[skip ci\]"; then
echo "Found [skip ci] - Vercel would skip this build"
else
echo "No [skip ci] found - Vercel would proceed with build"
fi
```
Test with both types of commits:
- Commits with `[skip ci]` should show "Vercel would skip this build"
- Commits without `[skip ci]` should show "Vercel would proceed with build"
</Step>
</Steps>
### How it works
1. **During task execution**: Open SWE includes `[skip ci]` in commit messages
2. **Vercel behavior**: Skips creating preview deployments for these commits (script exits with code 1)
3. **Task completion**: Open SWE pushes a final commit without `[skip ci]` to trigger deployment (script exits with code 0)
4. **Result**: Only one preview deployment per Open SWE task

View file

@ -50,3 +50,6 @@ OPEN_SWE_APP_URL="http://localhost:3000"
# Should be the same value as the one used in the web app, so that secrets
# encrypted in the web app can be decrypted in the agent.
SECRETS_ENCRYPTION_KEY=""
# Whether or not to append the string "[skip ci]" to the commit message.
# See the documentation for how to set this up: docs.langchain.com/labs/swe/setup/ci#skip-ci-until-last-commit
SKIP_CI_UNTIL_LAST_COMMIT="true"

View file

@ -10,6 +10,7 @@ import {
import {
checkoutBranchAndCommit,
getChangedFilesStatus,
pushEmptyCommit,
} from "../../../utils/github/git.js";
import {
createPullRequest,
@ -167,6 +168,12 @@ export async function openPullRequest(
);
}
if (process.env.SKIP_CI_UNTIL_LAST_COMMIT === "true") {
await pushEmptyCommit(state.targetRepository, sandbox, {
githubInstallationToken,
});
}
const { title, body } = toolCall.args as z.infer<typeof openPrTool.schema>;
const prForTask = getPullRequestNumberFromActiveTask(

View file

@ -169,6 +169,16 @@ export async function stashAndClearChanges(
}
}
function constructCommitMessage(): string {
const baseCommitMessage = "Apply patch";
const skipCiString = "[skip ci]";
const vercelSkipCi = process.env.SKIP_CI_UNTIL_LAST_COMMIT === "true";
if (vercelSkipCi) {
return `${baseCommitMessage} ${skipCiString}`;
}
return baseCommitMessage;
}
export async function checkoutBranchAndCommit(
config: GraphConfig,
targetRepository: TargetRepository,
@ -203,7 +213,12 @@ export async function checkoutBranchAndCommit(
}
const userName = `${botAppName}[bot]`;
const userEmail = `${botAppName}@users.noreply.github.com`;
await sandbox.git.commit(absoluteRepoDir, "Apply patch", userName, userEmail);
await sandbox.git.commit(
absoluteRepoDir,
constructCommitMessage(),
userName,
userEmail,
);
// Push the changes using the git API so it handles authentication for us.
const pushRes = await withRetry(
@ -301,6 +316,71 @@ export async function checkoutBranchAndCommit(
return { branchName, updatedTaskPlan };
}
export async function pushEmptyCommit(
targetRepository: TargetRepository,
sandbox: Sandbox,
options: {
githubInstallationToken: string;
},
) {
const botAppName = process.env.GITHUB_APP_NAME;
if (!botAppName) {
logger.error("GITHUB_APP_NAME environment variable is not set.");
throw new Error("GITHUB_APP_NAME environment variable is not set.");
}
const userName = `${botAppName}[bot]`;
const userEmail = `${botAppName}@users.noreply.github.com`;
try {
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
const setGitConfigRes = await sandbox.process.executeCommand(
`git config user.name "${userName}" && git config user.email "${userEmail}"`,
absoluteRepoDir,
undefined,
TIMEOUT_SEC,
);
if (setGitConfigRes.exitCode !== 0) {
logger.error(`Failed to set git config`, {
exitCode: setGitConfigRes.exitCode,
result: setGitConfigRes.result,
});
return;
}
const emptyCommitRes = await sandbox.process.executeCommand(
"git commit --allow-empty -m 'Empty commit to trigger CI'",
absoluteRepoDir,
undefined,
TIMEOUT_SEC,
);
if (emptyCommitRes.exitCode !== 0) {
logger.error(`Failed to push empty commit`, {
exitCode: emptyCommitRes.exitCode,
result: emptyCommitRes.result,
});
return;
}
await sandbox.git.push(
absoluteRepoDir,
"git",
options.githubInstallationToken,
);
logger.info("Successfully pushed empty commit");
} catch (e) {
const errorFields = getSandboxErrorFields(e);
logger.error(`Failed to push empty commit`, {
...(errorFields && { errorFields }),
...(e instanceof Error && {
name: e.name,
message: e.message,
stack: e.stack,
}),
});
}
}
export async function pullLatestChanges(
absoluteRepoDir: string,
sandbox: Sandbox,