Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1ce7c6d
Add the Test Companion action
naimish-browse Oct 30, 2026
d615c6a
test-companion: run git with a clean environment and no hooks
naimish-browse Oct 30, 2026
184603d
fix: scope the push credential to one URL the repo cannot rewrite
naimish-browse Oct 30, 2026
06807a9
fix: refuse to commit when the repository's git config can redirect o…
naimish-browse Oct 30, 2026
2facd08
fix: allow-list the git config instead of deny-listing it
naimish-browse Oct 30, 2026
9eb6650
fix: run the real git, not whatever PATH resolves to
naimish-browse Oct 30, 2026
3522e2e
docs: record what the pinned git binary does not survive
naimish-browse Oct 30, 2026
c5dd7e3
fix: an approved rca/fix/verdict lost the build it was meant to read
naimish-browse Oct 30, 2026
50f1f46
fix: the config check was reading our own -c flags back as tampering
naimish-browse Oct 30, 2026
817ca58
fix: a question asked in a pull request gets its answer in the pull r…
naimish-browse Oct 30, 2026
8f54b7c
fix: an unheld answer job runs in preview, not merely unheld
naimish-browse Oct 30, 2026
bc2c3ea
fix: pin upload-artifact to a commit, escape the marker sha in one place
naimish-browse Oct 30, 2026
0bbda92
refactor: no pattern is built from input any more
naimish-browse Oct 30, 2026
6adf348
fix: pin checkout in the starter, keep our own tag floating
naimish-browse Oct 30, 2026
708e694
fix: use the full rule id in the starter's nosemgrep
naimish-browse Oct 30, 2026
7dbe5cf
fix: the three blockers from the review of browserstack/vscode-ai-tes…
naimish-browse Oct 30, 2026
4e6df10
fix: the review findings from browserstack/vscode-ai-testing#3101
naimish-browse Oct 30, 2026
cab1236
fix: a second go says the plan was already run, not that the commit m…
naimish-browse Oct 30, 2026
491d1ac
fix: sign gh in, so a failed build can actually be read
naimish-browse Nov 2, 2026
c64de47
fix: keep gh's credentials inside the job that created them
naimish-browse Nov 2, 2026
f5ef874
fix: the already-signed-in guard was reading its own token
naimish-browse Nov 2, 2026
75d94b8
fix: the gh sign-in could not store, and took the run down with it
naimish-browse Nov 2, 2026
ab69d3f
fix: post every job's answer in the comment, not just two
naimish-browse Nov 2, 2026
b0fa7c1
fix: stop borrowed text forging a marker in our own comment
naimish-browse Nov 2, 2026
1db7ecd
fix: neutralise EVERY borrowed field, not just the obvious two
naimish-browse Nov 2, 2026
8258d8c
fix: a proposal printed the answer twice
naimish-browse Nov 2, 2026
82c9954
feat: say which request the comment is answering
naimish-browse Nov 2, 2026
3ff74d3
fix: give the dedupe claim a credential, so live mode can run
naimish-browse Nov 2, 2026
e1d2759
fix: a run that never ran must not spend the approval
naimish-browse Nov 2, 2026
74ca407
fix: "go" can say which plan it answers, and spends only that plan
naimish-browse Nov 3, 2026
69894aa
feat: remove `stop`
naimish-browse Nov 3, 2026
96bf2ae
feat: a run that BROKE now reddens the build by default
naimish-browse Nov 3, 2026
62a119e
fix: `go #<plan>` must not need the mention either
naimish-browse Nov 3, 2026
a451f4a
feat: mark each outcome, so the heading reads at a glance
naimish-browse Nov 3, 2026
b222051
fix: a test that could only ever pass on macOS
naimish-browse Nov 3, 2026
af9b32e
fix: eight findings from the review of vscode-ai-testing#3286
naimish-browse Nov 3, 2026
1f76aa9
fix: six findings from CodeRabbit on vscode-ai-testing#3286
naimish-browse Nov 3, 2026
a333c99
docs: clean the comments for a public repo
naimish-browse Nov 4, 2026
ddb7d18
feat: address the Action as @browserstack, and only as a whole word
naimish-browse Nov 5, 2026
c859119
ci: run the Test Companion Action's tests on every change to it
naimish-browse Nov 5, 2026
2fdb6f9
fix: "go" approves only when the comment opens with it
naimish-browse Nov 5, 2026
b6872e3
fix: neutralise markers in the saved-work note, approver note and war…
naimish-browse Nov 5, 2026
7fd120e
docs: fix the README quick start, build-effect and approval notes
naimish-browse Nov 5, 2026
ddb0f30
ci: the three zizmor findings on the new test workflow
naimish-browse Nov 5, 2026
044fb14
fix: run only a system gh, with a fresh settings folder per call
naimish-browse Nov 6, 2026
567f080
fix: close the review's security gaps, and tidy the public copy
naimish-browse Nov 6, 2026
9b0efdb
feat: run the agent as a separate, locked-down user
naimish-browse Nov 6, 2026
3ff27d0
fix: open a handed-back pull request against the real default branch
naimish-browse Nov 6, 2026
79a6de4
fix: close the relay's GraphQL bypass, and the review's other blockers
naimish-browse Nov 9, 2026
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
61 changes: 61 additions & 0 deletions .github/workflows/test-companion-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Runs the Test Companion Action's own unit tests.
#
# The Action runs on customers' machines with their credentials, so its tests
# have to run on every change to it.
#
# The `paths` filter is safe only while this check is NOT required. A required
# check with a paths filter never reports on a pull request that misses the
# filter, and then blocks that pull request from merging for ever. If this is
# ever made required, remove the filter.
name: Test Companion tests

on:
pull_request:
branches: [master]
paths:
- "test-companion/**"
- ".github/workflows/test-companion-tests.yml"
push:
branches: [master]
paths:
- "test-companion/**"

permissions:
contents: read

# One run per pull request or branch. A newer push makes the older run's
# result irrelevant, so it is cancelled rather than left to finish.
concurrency:
group: test-companion-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
unit-tests:
name: Test Companion unit tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# The tests need no git credentials, so none are left on disk.
persist-credentials: false

# The Action has no dependencies: Node's standard library plus `gh`.
# There is nothing to install.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"

# The exit code alone is not enough. When a test file throws while it
# loads (a missing import, say), `node --test` prints "fail 0" and
# exits 0, even though none of that file's tests ran. The only sign
# is a `not ok` line, so fail on that as well.
- name: Run the unit tests
working-directory: test-companion
run: |
set -o pipefail
node --test --test-reporter=tap test/*.test.mjs | tee test-output.tap
if grep -nE '^\s*not ok' test-output.tap; then
echo "::error::A test failed or a test file did not load. See the 'not ok' lines above."
exit 1
fi
5 changes: 5 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
* @browserstack/automate-public-repos

# The Test Companion action. Changes to it run an AI agent in customers' jobs,
# so they also need a review from its team.
/test-companion/ @browserstack/automate-public-repos @browserstack/test-companion
/.github/workflows/test-companion-tests.yml @browserstack/automate-public-repos @browserstack/test-companion
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ If you want to test your open source project on BrowserStack, then [sign-up here

* [setup-local](./setup-local): This Action downloads and starts the appropriate BrowserStackLocal binary, thereby creating a secure tunnel connection from the GitHub Actions runner environment to the BrowserStack device cloud. This secure tunnel will be used by the remote browsers in BrowserStack to access your web application hosted in the GitHub Actions runner environment. **You do not need this Action if the application to be tested is accessible over the public internet.**

* [test-companion](./test-companion): Ask BrowserStack Test Companion for test work from a pull request. Comment `@browserstack test this PR` and it proposes the tests it would write, then writes them once someone with write access replies `go`. Needs a Linux runner. See its [README](./test-companion/README.md) for setup.

## Prerequisites
* You should set your BrowserStack Username and Access Key as GitHub Secrets, i.e. `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY` respectively.

Expand Down
109 changes: 109 additions & 0 deletions test-companion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# test-companion

Ask BrowserStack Test Companion for test work from a pull request.

Comment `@browserstack test this PR` and it reads the change, proposes the
tests it would write, and writes them once someone replies `go`. The result
comes back as one comment and one check.

## Quick start

1. Add two secrets: `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY`.
2. Copy [`starter-workflow.yml`](starter-workflow.yml) to
`.github/workflows/test-companion.yml` in your repository, as-is.

What runs is decided by `testcompanion-pipeline.yml` in your repository, not by
the workflow file.

**Runner requirements:** a Linux runner (GitHub's `ubuntu-latest` works as is)
with Node 20 or later, the `gh` command installed system-wide (in `/usr/bin` or
`/usr/local/bin`), and password-free `sudo` for the job.

`sudo` is what keeps your tokens away from the AI agent. Test Companion runs
the agent as a separate user with no admin rights, which cannot read this
job's tokens, secrets or temp files. Its `gh` can only read from GitHub,
through a relay; every write (saving tests, opening a pull request) is made by
the Action itself after the run. On a runner without password-free `sudo` the
run stops and says why, unless you set `allow-unisolated: true`.

Use the starter file itself rather than writing your own. Besides the triggers,
it carries safeguards that are easy to leave out:

- a filter that skips comments not meant for Test Companion, so they don't start
a job;
- `persist-credentials: false` on checkout, so the workflow token is not left in
`.git/config` where commands the agent runs could read it;
- the concurrency rules that keep one run per pull request, and let closing a
pull request cancel its run.

## Pin the CLI version

`cli-version` is optional and you should set it once you know which version you
want.

Without a pin the CLI upgrades itself the first time it runs on a release day.
That spends your pipeline's time on a download, and it means two runs a day
apart are not the same software.

**Both lines or neither.** A version with no checksum is refused — "refusing an
unverified pinned install" — and the job stops before it reports anything. Take
both from the release you tested with.

```yaml
- uses: browserstack/github-actions/test-companion@v1.1.0
with:
username: ${{ secrets.BROWSERSTACK_USERNAME }}
access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
cli-version: "1.2.0"
cli-sha256: "<sha256 of that release for linux-amd64>"
```

## Inputs

| Input | Required | What it does |
|---|---|---|
| `username` | yes | BrowserStack username, from your secret store |
| `access-key` | yes | BrowserStack access key, from your secret store |
| `cli-version` | no | Pin the CLI. Strongly recommended — see above |
| `cli-sha256` | no | Checksum for the pinned version |
| `build-effect` | no | Whether this step goes red. `fail-on-break` (the default) only when the run itself broke, could not start or hit a limit, not when tests failed. `report` never. `fail` on anything that is not a success, test failures included. The Test Companion check always reports truthfully either way |
| `workflow` | no | Manual runs only; event-driven runs read the pipeline file |
| `preview` | no | Report what would happen and change nothing |
| `allow-unisolated` | no | `"true"` runs the agent as the job's own user when it cannot be locked down. It can then reach this job's tokens and secrets. Off by default |
| `app-id` / `app-private-key` | no | Use the GitHub App's identity. Keep the workflow's `permissions:` block anyway: the workflow token is still the fallback |

## Approving a plan

Before a job changes anything, Test Companion posts its plan and waits for
someone with write access to reply `go`. Read the plan before you reply.

Without the GitHub App, Test Companion posts as `github-actions[bot]`, the same
account every other workflow in your repository uses. It can only recognise its
own plans by that account and a hidden marker. So a different workflow that
copies outside text into a comment could imitate a plan. A reply of `go` only
ever approves a plan for the pull request's current commit. With the App, Test
Companion posts under its own account, which no other workflow can use.

## How the CLI is installed

Each run downloads the CLI installer from
`test-companion.browserstack.com/cli/install.sh`. The installer script itself
is not checked against a fixed checksum, so a run trusts that address. Pinning
`cli-version` with `cli-sha256` (above) makes the installer refuse any CLI
build other than the one you tested.

## What runs is not decided here

This file is awkward to change once it is in many repositories, so it stays
near-empty on purpose. Which jobs run, on which events, with which spending
limits, lives in `testcompanion-pipeline.yml` in your own repository, which you
can edit freely without touching the workflow above.

## Notes for maintainers

This is a **composite** action with **no dependencies** and no `dist/` bundle to
rebuild — unlike the JavaScript actions elsewhere in this repository. The
sources in `src/` are what run.

Tests: `cd test-companion && node --test`. They need no network and no
credentials.
Loading
Loading