-
Notifications
You must be signed in to change notification settings - Fork 92
feat: add Git repository check and automatic .gitignore append safety #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,7 +41,7 @@ jobs: | |
| app-id: ${{ secrets.TESTSPRITE_HOB_APP_ID || secrets.ALFHEIM_AGENT_APP_ID }} | ||
| private-key: ${{ secrets.TESTSPRITE_HOB_PRIVATE_KEY || secrets.ALFHEIM_AGENT_PRIVATE_KEY }} | ||
|
|
||
| - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.0.1 | ||
| - uses: actions/github-script@v8 | ||
| env: | ||
| APP_TOKEN: ${{ steps.app-token.outputs.token }} | ||
| with: | ||
|
|
@@ -51,7 +51,7 @@ jobs: | |
| const marker = process.env.MARKER; | ||
|
|
||
| const appClient = process.env.APP_TOKEN | ||
| ? require('@actions/github').getOctokit(process.env.APP_TOKEN) | ||
| ? getOctokit(process.env.APP_TOKEN) | ||
| : null; | ||
|
Comment on lines
53
to
55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== Workflow references ==\n'
rg -n "actions/github-script|getOctokit|APP_TOKEN" .github/workflows/ci-nudge.yml .github/workflows/pr-triage.yml
printf '\n== Relevant workflow snippets ==\n'
for f in .github/workflows/ci-nudge.yml .github/workflows/pr-triage.yml; do
echo "--- $f ---"
nl -ba "$f" | sed -n '1,220p' | rg -n "actions/github-script|getOctokit|APP_TOKEN|script:|with:"
done
printf '\n== Upstream github-script v8 source check ==\n'
python3 - <<'PY'
import urllib.request, re
url = "https://raw.githubusercontent.com/actions/github-script/v8.0.0/dist/index.js"
data = urllib.request.urlopen(url, timeout=20).read().decode()
for needle in ["getOctokit", "github-token", "script:", "context", "core"]:
print(f"{needle}: {'yes' if needle in data else 'no'}")
m = re.search(r"getOctokit", data)
print("first getOctokit index:", m.start() if m else None)
PYRepository: TestSprite/testsprite-cli Length of output: 974 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Workflow excerpts =="
python3 - <<'PY'
from pathlib import Path
for f in [Path('.github/workflows/ci-nudge.yml'), Path('.github/workflows/pr-triage.yml')]:
print(f"\n--- {f} ---")
lines = f.read_text().splitlines()
for i, line in enumerate(lines, 1):
if 'actions/github-script@v8' in line or 'APP_TOKEN' in line or 'getOctokit' in line:
start = max(1, i-4)
end = min(len(lines), i+4)
for j in range(start, end+1):
print(f"{j:4d}: {lines[j-1]}")
print()
PY
echo "== github-script v8 source: helper injection =="
python3 - <<'PY'
import urllib.request, re, sys
url = "https://raw.githubusercontent.com/actions/github-script/v8.0.0/dist/index.js"
data = urllib.request.urlopen(url, timeout=20).read().decode()
for needle in ["getOctokit", "context", "github-token", "with:", "script"]:
print(f"{needle}: {'found' if needle in data else 'missing'}")
for pat in [r"const\s+getOctokit", r"getOctokit\s*[:=]", r"inputs\.github-token"]:
m = re.search(pat, data)
print(pat, "->", bool(m), m.start() if m else None)
PYRepository: TestSprite/testsprite-cli Length of output: 6451 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import urllib.request, re
url = "https://raw.githubusercontent.com/actions/github-script/v8.0.0/dist/index.js"
data = urllib.request.urlopen(url, timeout=20).read().decode()
# Print short surrounding context around getOctokit occurrences.
for m in re.finditer(r'getOctokit', data):
s = max(0, m.start() - 220)
e = min(len(data), m.end() + 220)
print("..."+data[s:e]+"...\n")
break
PYRepository: TestSprite/testsprite-cli Length of output: 2757 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for f in [Path('.github/workflows/ci-nudge.yml'), Path('.github/workflows/pr-triage.yml')]:
print(f"\n--- {f} ---")
lines = f.read_text().splitlines()
for i, line in enumerate(lines, 1):
if 'actions/github-script@v8' in line or 'APP_TOKEN' in line or 'getOctokit' in line:
start = max(1, i-3)
end = min(len(lines), i+3)
for j in range(start, end+1):
print(f"{j:4d}: {lines[j-1]}")
print()
PY
python3 - <<'PY'
import urllib.request, re
url = "https://raw.githubusercontent.com/actions/github-script/v8.0.0/dist/index.js"
data = urllib.request.urlopen(url, timeout=20).read().decode()
for needle in ["getOctokit", "github-token", "script", "context"]:
print(f"{needle}: {'found' if needle in data else 'missing'}")
m = re.search(r'getOctokit', data)
if m:
s = max(0, m.start()-200)
e = min(len(data), m.end()+300)
print("\ncontext:\n")
print(data[s:e])
PYRepository: TestSprite/testsprite-cli Length of output: 5416 🌐 Web query:
💡 Result: In actions/github-script v8, the getOctokit function is not available as an injected factory function [1][2]. The getOctokit functionality was introduced in v9 [3][4]. In v8, users typically access the GitHub API via the pre-authenticated github client provided in the script context [5]. If you need a client with a different token in v8, you must manually import the Octokit library or the Citations:
Both workflows will hit
Restore 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| async function write(fn) { | ||
| if (appClient) { | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { join } from 'node:path'; | ||
| import { | ||
| isInsideGitRepo, | ||
| isTestspriteIgnored, | ||
| checkTestspriteIgnored, | ||
| ensureTestspriteIgnored, | ||
| } from './git-utils.js'; | ||
|
|
||
| describe('git-utils', () => { | ||
| describe('isInsideGitRepo', () => { | ||
| it('returns true if .git exists in the current directory', () => { | ||
| const mockExists = (p: string) => p === join('/foo/bar', '.git'); | ||
| expect(isInsideGitRepo('/foo/bar', { existsSync: mockExists })).toBe(true); | ||
| }); | ||
|
|
||
| it('returns true if .git exists in a parent directory', () => { | ||
| const mockExists = (p: string) => p === join('/foo', '.git'); | ||
| expect(isInsideGitRepo('/foo/bar/baz', { existsSync: mockExists })).toBe(true); | ||
| }); | ||
|
|
||
| it('returns false if .git is not found anywhere up the tree', () => { | ||
| const mockExists = () => false; | ||
| expect(isInsideGitRepo('/foo/bar/baz', { existsSync: mockExists })).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('isTestspriteIgnored', () => { | ||
| it('returns false for empty content', () => { | ||
| expect(isTestspriteIgnored('')).toBe(false); | ||
| }); | ||
|
|
||
| it('returns true if .testsprite is explicitly ignored', () => { | ||
| expect(isTestspriteIgnored('.testsprite')).toBe(true); | ||
| expect(isTestspriteIgnored('.testsprite/')).toBe(true); | ||
| expect(isTestspriteIgnored('**/.testsprite')).toBe(true); | ||
| expect(isTestspriteIgnored('**/.testsprite/')).toBe(true); | ||
| }); | ||
|
|
||
| it('returns true if .testsprite subfolders or files are ignored', () => { | ||
| expect(isTestspriteIgnored('.testsprite/*')).toBe(true); | ||
| expect(isTestspriteIgnored('**/.testsprite/runs')).toBe(true); | ||
| }); | ||
|
|
||
| it('ignores comments and empty lines', () => { | ||
| expect(isTestspriteIgnored('# .testsprite/')).toBe(false); | ||
| expect(isTestspriteIgnored(' # comment\n.testsprite/')).toBe(true); | ||
| }); | ||
|
|
||
| it('returns false if unrelated folders are ignored', () => { | ||
| expect(isTestspriteIgnored('node_modules/\ndist/')).toBe(false); | ||
| }); | ||
|
|
||
| it('correctly handles negation patterns (un-ignoring)', () => { | ||
| expect(isTestspriteIgnored('.testsprite/\n!.testsprite/')).toBe(false); | ||
| expect(isTestspriteIgnored('!.testsprite/\n.testsprite/')).toBe(true); | ||
| expect(isTestspriteIgnored('**/.testsprite/\n!**/.testsprite/')).toBe(false); | ||
| expect(isTestspriteIgnored('.testsprite/*\n!.testsprite/')).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('checkTestspriteIgnored', () => { | ||
| it('returns false if .gitignore does not exist', async () => { | ||
| const mockExists = () => false; | ||
| const mockRead = async () => ''; | ||
| await expect(checkTestspriteIgnored('/foo', { existsSync: mockExists, readFile: mockRead })).resolves.toBe(false); | ||
| }); | ||
|
|
||
| it('returns true if .gitignore exists and ignores .testsprite', async () => { | ||
| const mockExists = (p: string) => p === join('/foo', '.gitignore'); | ||
| const mockRead = async () => 'dist/\n.testsprite/'; | ||
| await expect(checkTestspriteIgnored('/foo', { existsSync: mockExists, readFile: mockRead })).resolves.toBe(true); | ||
| }); | ||
|
|
||
| it('returns false if .gitignore exists but does not ignore .testsprite', async () => { | ||
| const mockExists = (p: string) => p === join('/foo', '.gitignore'); | ||
| const mockRead = async () => 'dist/'; | ||
| await expect(checkTestspriteIgnored('/foo', { existsSync: mockExists, readFile: mockRead })).resolves.toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('ensureTestspriteIgnored', () => { | ||
| it('creates .gitignore and appends .testsprite if file does not exist', async () => { | ||
| let writtenContent = ''; | ||
| const mockExists = () => false; | ||
| const mockRead = async () => ''; | ||
| const mockWrite = async (p: string, content: string) => { | ||
| writtenContent = content; | ||
| }; | ||
|
|
||
| const result = await ensureTestspriteIgnored('/foo', { | ||
| existsSync: mockExists, | ||
| readFile: mockRead, | ||
| writeFile: mockWrite, | ||
| }); | ||
|
|
||
| expect(result).toBe(true); | ||
| expect(writtenContent).toContain('.testsprite/'); | ||
| }); | ||
|
|
||
| it('appends .testsprite if .gitignore exists but does not ignore it', async () => { | ||
| let writtenContent = ''; | ||
| const mockExists = () => true; | ||
| const mockRead = async () => 'node_modules/\ndist/'; | ||
| const mockWrite = async (p: string, content: string) => { | ||
| writtenContent = content; | ||
| }; | ||
|
|
||
| const result = await ensureTestspriteIgnored('/foo', { | ||
| existsSync: mockExists, | ||
| readFile: mockRead, | ||
| writeFile: mockWrite, | ||
| }); | ||
|
|
||
| expect(result).toBe(true); | ||
| expect(writtenContent).toBe('node_modules/\ndist/\n\n# TestSprite failure artifacts\n.testsprite/\n'); | ||
| }); | ||
|
|
||
| it('does not modify .gitignore if .testsprite is already ignored', async () => { | ||
| let calledWrite = false; | ||
| const mockExists = () => true; | ||
| const mockRead = async () => 'node_modules/\n.testsprite/'; | ||
| const mockWrite = async () => { | ||
| calledWrite = true; | ||
| }; | ||
|
|
||
| const result = await ensureTestspriteIgnored('/foo', { | ||
| existsSync: mockExists, | ||
| readFile: mockRead, | ||
| writeFile: mockWrite, | ||
| }); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(calledWrite).toBe(false); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Pin all external action references immutably.
Each workflow uses the mutable
actions/github-script@v8tag:.github/workflows/ci-nudge.yml#L44-L44: pinactions/github-scriptto the exact 40-character SHA for the selected v8 release..github/workflows/issue-triage.yml#L52-L52: pinactions/github-scriptto the exact 40-character SHA for the selected v8 release..github/workflows/pr-triage.yml#L64-L64: pinactions/github-scriptto the exact 40-character SHA for the selected v8 release.📍 Affects 3 files
.github/workflows/ci-nudge.yml#L44-L44(this comment).github/workflows/issue-triage.yml#L52-L52.github/workflows/pr-triage.yml#L64-L64🤖 Prompt for AI Agents
Source: Path instructions