Skip to content

Commit 7b4937b

Browse files
authored
Merge pull request #259 from TestSprite/release/v0.4.0
release: v0.4.0
2 parents 60d55e4 + e91c5a1 commit 7b4937b

58 files changed

Lines changed: 4410 additions & 327 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# backport-dispatch.yml — OPTIONAL latency optimization for DEV-352's
2+
# auto-backport bot. Authored in atlas (release/public path), ships to the
3+
# PUBLIC repo via the snapshot per invariant I0 — this workflow only ever
4+
# runs on the public repo, never on atlas (see the repo guard below).
5+
#
6+
# NOT YET ARMED (2026-07-15): the atlas-side auto-backport.yml has a
7+
# scheduled SWEEP (every 30 min) that scans merged public PRs anonymously
8+
# and backports anything unabsorbed — the whole pipeline is fully
9+
# functional with ZERO public-repo credentials via that path alone. This
10+
# workflow instantly notifies atlas the moment a PR merges instead of
11+
# waiting for the next sweep (seconds vs. up to 30 minutes) — a nice-to-have,
12+
# not a requirement.
13+
#
14+
# Arming it means placing a GitHub App credential with contents:write on
15+
# the PRIVATE atlas repo into THIS PUBLIC repo's secrets — a real trust
16+
# decision (a compromised public-repo secret store would let an attacker
17+
# push to atlas) that is deliberately left to the operator, not decided by
18+
# this file. Until TESTSPRITE_HOB_APP_ID/TESTSPRITE_HOB_PRIVATE_KEY (or the
19+
# ALFHEIM_AGENT_* fallback) exist on the PUBLIC repo with contents:write on
20+
# atlas, this workflow no-ops cleanly (see the "not armed" step) — it does
21+
# NOT fail loud, so it stays quiet for every contributor watching the
22+
# Actions tab until the operator decides to enable it.
23+
#
24+
# SECURITY: this workflow reads ONLY event metadata (PR number, merge SHA,
25+
# base ref) — it never checks out the merged PR's code. That is what makes
26+
# `pull_request_target` safe here: the base-repo context (secrets, a
27+
# write-capable token) is required because a fork-originated merged PR gets
28+
# ZERO secrets under a plain `pull_request` trigger, even for the `closed`
29+
# activity type — but nothing in this job ever runs untrusted fork code, so
30+
# the classic pull_request_target RCE/secret-exfiltration risk (checking
31+
# out and executing the fork's own head ref under a privileged context)
32+
# does not apply. Do not add actions/checkout to this workflow.
33+
name: Notify atlas of a merged public PR
34+
35+
on:
36+
pull_request_target:
37+
types: [closed]
38+
39+
permissions:
40+
contents: read
41+
42+
jobs:
43+
notify:
44+
if: github.repository == 'TestSprite/testsprite-cli' && github.event.pull_request.merged == true
45+
runs-on: ubuntu-latest
46+
steps:
47+
- id: app-token
48+
continue-on-error: true
49+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
50+
with:
51+
app-id: ${{ secrets.TESTSPRITE_HOB_APP_ID || secrets.ALFHEIM_AGENT_APP_ID }}
52+
private-key: ${{ secrets.TESTSPRITE_HOB_PRIVATE_KEY || secrets.ALFHEIM_AGENT_PRIVATE_KEY }}
53+
owner: TestSprite
54+
repositories: testsprite-cli-atlas
55+
# Minimum required for POST /repos/{owner}/{repo}/dispatches (verified
56+
# against docs.github.com/en/rest/using-the-rest-api/permissions-required-for-github-apps —
57+
# the endpoint is gated on Contents:write, not Contents:read).
58+
permission-contents: write
59+
60+
- name: Dispatch to atlas
61+
if: steps.app-token.outputs.token != ''
62+
env:
63+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
64+
# These two fields are fork-influenced (merge_commit_sha/base.ref come
65+
# from a merged PR's event payload) — routed through env instead of
66+
# spliced into the run: string to avoid the GHA script-injection class
67+
# (a maliciously-crafted value could otherwise break out of the -F
68+
# argument and inject arbitrary shell). PR_NUMBER is left inline: it is
69+
# a GitHub-assigned integer, not attacker-authorable content.
70+
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
71+
BASE_REF: ${{ github.event.pull_request.base.ref }}
72+
run: |
73+
set -euo pipefail
74+
gh api repos/TestSprite/testsprite-cli-atlas/dispatches \
75+
-f event_type=public-pr-merged \
76+
-F 'client_payload[pr_number]=${{ github.event.pull_request.number }}' \
77+
-F "client_payload[merge_commit_sha]=$MERGE_SHA" \
78+
-F "client_payload[base_ref]=$BASE_REF"
79+
80+
- name: Not armed yet — the atlas sweep will pick this up
81+
if: steps.app-token.outputs.token == ''
82+
run: |
83+
echo "::notice::No cross-repo credential configured on this repo yet — atlas's scheduled sweep (auto-backport.yml, every 30 min) will pick up this merge instead of an instant dispatch. See DEV-352 / DEV-348 / DEV-350 for the operator decision to arm this."

.github/workflows/ci-nudge.yml

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
# CI workflows (CI + Test Coverage) can complete in any order.
1010
#
1111
# Bot identity: same App-token-first / GITHUB_TOKEN-fallback pattern as
12-
# pr-triage.yml (the App needs the "Pull requests: Read & write" permission to
13-
# post as testsprite-hob[bot]; until then comments come from github-actions[bot]).
12+
# pr-triage.yml. The App has had the "Pull requests: Read & write" permission
13+
# since 2026-07-02 (corrected 2026-07-15; this comment previously said the
14+
# permission was still pending) — the fallback path stays as defense-in-depth
15+
# for whenever the App/secrets are absent.
1416
name: CI failure nudge
1517

1618
on:
@@ -40,8 +42,20 @@ jobs:
4042
with:
4143
app-id: ${{ secrets.TESTSPRITE_HOB_APP_ID || secrets.ALFHEIM_AGENT_APP_ID }}
4244
private-key: ${{ secrets.TESTSPRITE_HOB_PRIVATE_KEY || secrets.ALFHEIM_AGENT_PRIVATE_KEY }}
45+
# The script below only ever calls the App client (`appClient`) for
46+
# rest.issues.updateComment/createComment — PR comments ride the
47+
# issues API (see the header comment above). All reads (checks,
48+
# pulls, commit->PR lookup) go through the ambient `github` client,
49+
# governed by this workflow's top-level `permissions:` block, not
50+
# this minted token. Scoping to checks:read/pull-requests:write here
51+
# (matching the job's top-level block literally) would risk the mint
52+
# itself failing if the App's installation doesn't hold Checks
53+
# permission — which would silently drop the App-token path
54+
# entirely (continue-on-error swallows the failure) and always fall
55+
# back to github-actions[bot], defeating this bot's own purpose.
56+
permission-issues: write
4357

44-
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.0.1
58+
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
4559
env:
4660
APP_TOKEN: ${{ steps.app-token.outputs.token }}
4761
with:
@@ -82,14 +96,20 @@ jobs:
8296
'Build': 'run `npm run build` and fix the compile errors',
8397
'Local E2E Tests': 'run `npm run test:e2e` (it builds first)',
8498
'Coverage (>= 80%)': 'run `npm run test:coverage` — new code needs tests until every metric is back at 80%',
99+
'Secret scan (gitleaks)': 'run `gitleaks detect --no-git --redact --source .` locally (config: .gitleaks.toml) and remove the secret — or, for a provable false positive, add a narrowly-anchored allowlist regex',
85100
};
101+
// Matrix jobs publish check runs as "Unit Tests (Node 20)" etc. —
102+
// exact lookup alone would silently skip them (and the nudge would
103+
// say "all green" while they are red). Try exact first, then the
104+
// name with a trailing parenthetical stripped (review round 2).
105+
const fixFor = (name) => FIX[name] ?? FIX[name.replace(/\s*\([^)]*\)$/, '')];
86106
87107
// Recompute full CI state from this SHA's check runs, narrowed to the
88108
// CI jobs above — other workflows (e.g. pr-triage) also attach
89109
// github-actions check runs to the PR head and must not count here.
90110
const checks = await github.paginate(github.rest.checks.listForRef,
91111
{ owner, repo, ref: run.head_sha, per_page: 100 });
92-
const ours = checks.filter(c => c.app && c.app.slug === 'github-actions' && FIX[c.name]);
112+
const ours = checks.filter(c => c.app && c.app.slug === 'github-actions' && fixFor(c.name));
93113
const failing = ours.filter(c => ['failure', 'timed_out'].includes(c.conclusion));
94114
const pending = ours.filter(c => c.status !== 'completed');
95115
@@ -110,7 +130,7 @@ jobs:
110130
const lines = failing
111131
.sort((a, b) => a.name.localeCompare(b.name))
112132
.map(c => {
113-
const fix = FIX[c.name] || 'see the logs for details';
133+
const fix = fixFor(c.name) || 'see the logs for details';
114134
return `- **${c.name}** — ${fix} ([logs](${c.html_url}))`;
115135
});
116136
const body = `${marker}\nThanks, @${pr.user.login}! CI is red on this PR — `

.github/workflows/ci.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
runs-on: ubuntu-latest
1818
steps:
1919
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
20+
with:
21+
persist-credentials: false
2022

2123
- name: Setup Node.js
2224
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
@@ -37,6 +39,8 @@ jobs:
3739
runs-on: ubuntu-latest
3840
steps:
3941
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
42+
with:
43+
persist-credentials: false
4044

4145
- name: Setup Node.js
4246
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
@@ -55,6 +59,8 @@ jobs:
5559
node-version: [20, 22]
5660
steps:
5761
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
62+
with:
63+
persist-credentials: false
5864

5965
- name: Setup Node.js
6066
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
@@ -68,6 +74,27 @@ jobs:
6874
env:
6975
CI: true
7076

77+
test-windows:
78+
name: Unit Tests (Windows)
79+
runs-on: windows-latest
80+
steps:
81+
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
82+
with:
83+
persist-credentials: false
84+
85+
- name: Setup Node.js
86+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
87+
with:
88+
node-version: 22
89+
cache: 'npm'
90+
91+
- run: npm ci
92+
- run: npm run build
93+
- run: npm run typecheck
94+
- run: npm test
95+
env:
96+
CI: true
97+
7198
build:
7299
name: Build (Node ${{ matrix.node-version }})
73100
runs-on: ubuntu-latest
@@ -76,6 +103,8 @@ jobs:
76103
node-version: [20, 22]
77104
steps:
78105
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
106+
with:
107+
persist-credentials: false
79108

80109
- name: Setup Node.js
81110
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
@@ -96,6 +125,8 @@ jobs:
96125
runs-on: ubuntu-latest
97126
steps:
98127
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
128+
with:
129+
persist-credentials: false
99130

100131
- name: Setup Node.js
101132
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
@@ -107,3 +138,36 @@ jobs:
107138
- run: npm run test:e2e
108139
env:
109140
CI: true
141+
142+
gitleaks:
143+
name: Secret scan (gitleaks)
144+
runs-on: ubuntu-latest
145+
steps:
146+
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
147+
with:
148+
persist-credentials: false
149+
150+
# Same pinned version + checksum-verified install as
151+
# divergence-sentinel.yml (the more recent/secure of the two gitleaks
152+
# invocations already in this repo — release-build.yml pins an older
153+
# 8.21.2 with no checksum check). Continuous, per-PR/per-push gate: a
154+
# working-tree scan (--no-git), not a full-history scan — history
155+
# scanning is too slow to run on every PR, and this mirrors the mode
156+
# scripts/make-public-snapshot.sh already uses for the release-time scan
157+
# (`gitleaks detect --no-git --no-banner --redact --source <tree>`).
158+
- name: Install gitleaks
159+
env:
160+
GITLEAKS_VERSION: '8.28.0'
161+
run: |
162+
set -euo pipefail
163+
BASE="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}"
164+
TARBALL="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
165+
curl -sSL -o "$TARBALL" "${BASE}/${TARBALL}"
166+
curl -sSL -o checksums.txt "${BASE}/gitleaks_${GITLEAKS_VERSION}_checksums.txt"
167+
grep " ${TARBALL}\$" checksums.txt | sha256sum -c -
168+
tar -xzf "$TARBALL" gitleaks
169+
sudo mv gitleaks /usr/local/bin/gitleaks
170+
gitleaks version
171+
172+
- name: Scan working tree for secrets
173+
run: gitleaks detect --no-git --no-banner --redact --source .

.github/workflows/issue-triage.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,18 @@ env:
3131

3232
jobs:
3333
triage:
34+
# Public repo only — this file also lives in the private mirror (atlas),
35+
# where issue-first claiming doesn't apply; the three sibling bots
36+
# (pr-triage.yml, ci-nudge.yml, stale.yml) all carry this same fence and
37+
# this one was missing it (found in the 2026-07-15 OSS-P2 audit — the
38+
# /assign bot was live on atlas too until this fix).
3439
# issues only (issue_comment also fires on PRs), and never react to a bot's own
3540
# comment — incl. our own testsprite-hob[bot], which (unlike GITHUB_TOKEN) would
3641
# otherwise re-trigger this workflow. `type == 'Bot'` covers both bot identities.
37-
if: ${{ !github.event.issue.pull_request && github.event.comment.user.type != 'Bot' }}
42+
if: >-
43+
github.repository == 'TestSprite/testsprite-cli' &&
44+
!github.event.issue.pull_request &&
45+
github.event.comment.user.type != 'Bot'
3846
runs-on: ubuntu-latest
3947
steps:
4048
# Mint a token for the testsprite-hob App so comments post as testsprite-hob[bot].
@@ -49,7 +57,7 @@ jobs:
4957
# of inheriting the App's full installation permissions.
5058
permission-issues: write
5159

52-
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.0.1
60+
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
5361
with:
5462
# App token when available (→ testsprite-hob[bot]); else the default
5563
# GITHUB_TOKEN (→ github-actions[bot]). Either way the logic is identical.

.github/workflows/pr-triage.yml

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
# Runs with NO checkout — metadata-only, so it is safe under pull_request_target
1414
# (which is required for fork PRs to get a write-capable token).
1515
#
16-
# Bot identity: posts as `testsprite-hob[bot]` when the App token works. The
17-
# App currently has Issues R/W + Metadata only — commenting/labeling a PULL
18-
# REQUEST needs the "Pull requests: Read & write" App permission (issues-API
19-
# endpoints are permission-checked by target type). Until an org admin adds
20-
# that permission and re-approves the installation, every call gracefully falls
21-
# back to the default GITHUB_TOKEN and posts as github-actions[bot].
16+
# Bot identity: posts as `testsprite-hob[bot]` when the App token works (the
17+
# App has had the "Pull requests: Read & write" permission since 2026-07-02 —
18+
# corrected 2026-07-15; this comment previously said the permission was still
19+
# pending). The App-token-first / GITHUB_TOKEN-fallback code path below is
20+
# kept regardless, as defense-in-depth for whenever the App/secrets are
21+
# absent or a future installation loses the permission.
2222
# Secrets: TESTSPRITE_HOB_* preferred; the ALFHEIM_AGENT_* fallbacks are the
2323
# original names from before the App was renamed (same App ID + key).
2424
name: PR triage (issue-link gate)
@@ -60,8 +60,18 @@ jobs:
6060
with:
6161
app-id: ${{ secrets.TESTSPRITE_HOB_APP_ID || secrets.ALFHEIM_AGENT_APP_ID }}
6262
private-key: ${{ secrets.TESTSPRITE_HOB_PRIVATE_KEY || secrets.ALFHEIM_AGENT_PRIVATE_KEY }}
63+
# Every write() call below (createComment/updateComment/addLabels/
64+
# removeLabel) is an `issues.*` Octokit method — GitHub gates all
65+
# four (comments AND labels, even on a PR number) on the "Issues"
66+
# repository permission, not "Pull requests" (verified against
67+
# docs.github.com/en/rest/using-the-rest-api/permissions-required-for-github-apps).
68+
# Scoping to issues:write alone is therefore both sufficient and
69+
# minimal — narrower than this job's top-level `permissions:` block,
70+
# which also carries pull-requests:write for the ambient-token
71+
# fallback path (unused by this specific App-token mint).
72+
permission-issues: write
6373

64-
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.0.1
74+
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
6575
env:
6676
APP_TOKEN: ${{ steps.app-token.outputs.token }}
6777
with:

.github/workflows/release.yaml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,33 @@ jobs:
1414
with:
1515
node-version: 22
1616
registry-url: 'https://registry.npmjs.org'
17-
# npm trusted publishing (OIDC) requires npm >= 11.5.1; Node 22 bundles npm 10.x
18-
- run: npm install -g npm@latest
17+
# npm trusted publishing (OIDC) requires npm >= 11.5.1; Node 22 never bundles
18+
# that (it ships 10.9.x). Pin an EXACT version rather than floating on
19+
# @latest: npm 12.0.0 shipped with a broken workspace symlink that pulled the
20+
# wrong `sigstore` dependency into its own release tarball, so every
21+
# `npm publish --provenance` failed with `Cannot find module 'sigstore'`
22+
# (npm/cli#9722) — this is exactly what our v0.2.0 and v0.3.0 release.yaml
23+
# runs hit, both AFTER `npm install -g npm@latest` reported success. Fixed
24+
# in 12.0.1 (2026-07-10); bump this pin deliberately, never float it again.
25+
- run: npm install -g npm@11.6.2
1926
- run: npm ci
2027
- run: npm run lint
2128
- run: npm run typecheck
2229
- run: npm run format:check
2330
- run: npm run test:coverage
2431
- run: npm run build
25-
# Auth via npm trusted publishing (OIDC) — no token needed; provenance is implied
26-
- run: npm publish --provenance --access public
32+
# Auth via npm trusted publishing (OIDC) — no token needed; provenance is
33+
# implied. A prerelease tag (e.g. v0.3.0-rc.1, contains "-") must publish
34+
# under an explicit dist-tag: current npm already refuses an implicit
35+
# `latest` for a prerelease version, but failing mid-release is worse than
36+
# not needing the guard, so this still passes --tag itself rather than
37+
# relying on that enforcement alone.
38+
- name: Publish
39+
env:
40+
REF_NAME: ${{ github.ref_name }}
41+
run: |
42+
if [[ "$REF_NAME" == *-* ]]; then
43+
npm publish --provenance --access public --tag next
44+
else
45+
npm publish --provenance --access public
46+
fi

.github/workflows/test-coverage.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
19+
with:
20+
persist-credentials: false
1921

2022
- name: Setup Node.js
2123
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0

0 commit comments

Comments
 (0)