Skip to content

Commit ffc24c7

Browse files
Merge branch 'main' into rachit367/fix-search-context-crash
2 parents e0ac451 + b7c3158 commit ffc24c7

441 files changed

Lines changed: 37719 additions & 5813 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/gh-stack/SKILL.md

Lines changed: 872 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
name: Check Prisma Migrations
2+
description: >-
3+
Verify Prisma migrations apply cleanly in order and reproduce schema.prisma
4+
(drift check), and that no new migration predates the latest on the base
5+
branch (ordering check). Designed to be embedded in an existing job so its
6+
failure turns that job's status red.
7+
8+
inputs:
9+
base-ref:
10+
description: >-
11+
Base git ref to diff migrations against (e.g. a PR's base branch). When
12+
set, the action skips work on PRs that don't touch migrations and runs the
13+
ordering check. When empty (release builds), the drift check always runs
14+
and the ordering check is skipped.
15+
required: false
16+
default: ""
17+
18+
runs:
19+
using: composite
20+
steps:
21+
- name: Detect Prisma changes
22+
id: detect
23+
shell: bash
24+
run: |
25+
if [ -z "${{ inputs.base-ref }}" ]; then
26+
echo "changed=true" >> "$GITHUB_OUTPUT"
27+
echo "No base-ref provided — running drift check unconditionally."
28+
exit 0
29+
fi
30+
git fetch --no-tags --depth=1 origin "+refs/heads/${{ inputs.base-ref }}:refs/remotes/origin/${{ inputs.base-ref }}"
31+
if git diff --name-only "origin/${{ inputs.base-ref }}" HEAD | grep -q '^packages/db/prisma/'; then
32+
echo "changed=true" >> "$GITHUB_OUTPUT"
33+
echo "Prisma changes detected — running migration checks."
34+
else
35+
echo "changed=false" >> "$GITHUB_OUTPUT"
36+
echo "No Prisma changes — skipping migration checks."
37+
fi
38+
39+
- name: Start Postgres
40+
if: steps.detect.outputs.changed == 'true'
41+
shell: bash
42+
run: |
43+
docker run -d --name prisma-check-pg \
44+
-e POSTGRES_USER=postgres \
45+
-e POSTGRES_PASSWORD=postgres \
46+
-e POSTGRES_DB=sourcebot \
47+
-p 5432:5432 postgres:16
48+
for i in $(seq 1 30); do
49+
if docker exec prisma-check-pg pg_isready -U postgres -q; then
50+
echo "Postgres ready."
51+
exit 0
52+
fi
53+
sleep 2
54+
done
55+
echo "Postgres failed to become ready." && exit 1
56+
57+
- name: Use Node.js
58+
if: steps.detect.outputs.changed == 'true'
59+
uses: actions/setup-node@v4
60+
with:
61+
node-version: "20.x"
62+
63+
- name: Install
64+
if: steps.detect.outputs.changed == 'true'
65+
shell: bash
66+
run: yarn install --frozen-lockfile
67+
68+
# Check 1: migrations apply cleanly in order AND reproduce schema.prisma.
69+
# `migrate deploy` fails if a migration is broken or applies out of sequence;
70+
# `migrate diff` exits 2 when the applied history drifts from the schema.
71+
- name: Apply migrations
72+
if: steps.detect.outputs.changed == 'true'
73+
shell: bash
74+
working-directory: packages/db
75+
env:
76+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot
77+
run: yarn prisma migrate deploy
78+
79+
- name: Check for schema drift
80+
if: steps.detect.outputs.changed == 'true'
81+
shell: bash
82+
working-directory: packages/db
83+
env:
84+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot
85+
run: |
86+
yarn prisma migrate diff \
87+
--from-url "$DATABASE_URL" \
88+
--to-schema-datamodel prisma/schema.prisma \
89+
--exit-code \
90+
&& echo "✅ No drift: migrations reproduce schema.prisma" \
91+
|| (echo "❌ schema.prisma has changes not captured in a migration. Run: yarn dev:prisma:migrate:dev --name <name>" && exit 1)
92+
93+
# Check 2 (PRs only): no new migration predates the latest on the base branch.
94+
- name: Check migration ordering
95+
if: steps.detect.outputs.changed == 'true' && inputs.base-ref != ''
96+
shell: bash
97+
run: |
98+
MIG_DIR=packages/db/prisma/migrations
99+
BASE="origin/${{ inputs.base-ref }}"
100+
LATEST_ON_BASE=$(git ls-tree -r --name-only "$BASE" -- "$MIG_DIR" \
101+
| sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort | tail -1)
102+
echo "Latest migration on ${{ inputs.base-ref }}: ${LATEST_ON_BASE:-<none>}"
103+
NEW=$(comm -23 \
104+
<(ls "$MIG_DIR" | sed -n 's/^\([0-9]\{14\}\)_.*/\1/p' | sort -u) \
105+
<(git ls-tree -r --name-only "$BASE" -- "$MIG_DIR" | sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort -u))
106+
FAIL=0
107+
for ts in $NEW; do
108+
if [ -n "$LATEST_ON_BASE" ] && [ "$ts" -lt "$LATEST_ON_BASE" ]; then
109+
echo "❌ New migration $ts predates latest migration on ${{ inputs.base-ref }} ($LATEST_ON_BASE). Rename it with a current timestamp."
110+
FAIL=1
111+
fi
112+
done
113+
[ "$FAIL" -eq 0 ] && echo "✅ Migration ordering OK"
114+
exit $FAIL
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
def finding_id($prefix):
2+
if ((.title // "") | startswith($prefix + " ")) then
3+
(.title | ltrimstr($prefix + " ") | split(": "))
4+
| if length > 1 then .[0] else "" end
5+
else
6+
""
7+
end;
8+
9+
($findings[0].cves | map(.cveId)) as $currentIds
10+
| map(
11+
. + { findingId: finding_id($prefix) }
12+
| .findingId as $findingId
13+
| if .findingId == "" then
14+
. + { action: "ignore" }
15+
elif ($skipCodeql and (.findingId | startswith("codeql:"))) then
16+
. + { action: "ignore" }
17+
elif ($currentIds | index($findingId)) != null then
18+
. + { action: "keep" }
19+
else
20+
. + { action: "close" }
21+
end
22+
)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [[ -z "${LINEAR_API_KEY:-}" ]]; then
5+
echo "LINEAR_API_KEY is required" >&2
6+
exit 1
7+
fi
8+
9+
payload=$(cat)
10+
configured_attempts="${LINEAR_GRAPHQL_ATTEMPTS:-4}"
11+
retry_delay="${LINEAR_GRAPHQL_RETRY_DELAY_SECONDS:-2}"
12+
endpoint="${LINEAR_GRAPHQL_ENDPOINT:-https://api.linear.app/graphql}"
13+
is_mutation=$(jq -r '(.query // "") | test("^\\s*mutation(?:\\s|\\(|\\{)")' <<<"$payload")
14+
15+
# Retrying a mutation after an ambiguous transport failure can replay a write
16+
# that Linear already committed. Queries are safe to retry; mutations fail
17+
# visibly after one attempt and rely on the workflow's reconciliation pass.
18+
if [[ "$is_mutation" == "true" ]]; then
19+
attempts=1
20+
else
21+
attempts="$configured_attempts"
22+
fi
23+
24+
for ((attempt = 1; attempt <= attempts; attempt++)); do
25+
response_file=$(mktemp)
26+
http_code=""
27+
28+
if http_code=$(curl \
29+
--silent \
30+
--show-error \
31+
--output "$response_file" \
32+
--write-out '%{http_code}' \
33+
--connect-timeout 10 \
34+
--max-time 45 \
35+
-X POST "$endpoint" \
36+
-H "Content-Type: application/json" \
37+
-H "Authorization: $LINEAR_API_KEY" \
38+
-d "$payload"); then
39+
response=$(<"$response_file")
40+
rm -f "$response_file"
41+
42+
if [[ "$http_code" =~ ^2[0-9][0-9]$ ]] && jq -e . >/dev/null 2>&1 <<<"$response"; then
43+
printf '%s' "$response"
44+
exit 0
45+
fi
46+
47+
if [[ "$http_code" =~ ^(408|429|5[0-9][0-9])$ ]]; then
48+
echo "Linear GraphQL returned transient HTTP $http_code (attempt $attempt/$attempts)." >&2
49+
elif jq -e . >/dev/null 2>&1 <<<"$response"; then
50+
# Preserve structured non-retryable errors so the workflow can report
51+
# the GraphQL response rather than replacing it with a transport error.
52+
printf '%s' "$response"
53+
exit 0
54+
else
55+
echo "Linear GraphQL returned a non-JSON response (HTTP $http_code, attempt $attempt/$attempts)." >&2
56+
fi
57+
else
58+
curl_status=$?
59+
response=$(<"$response_file")
60+
rm -f "$response_file"
61+
echo "Linear GraphQL request failed (curl $curl_status, HTTP ${http_code:-unknown}, attempt $attempt/$attempts)." >&2
62+
fi
63+
64+
if ((attempt < attempts)); then
65+
sleep "$retry_delay"
66+
fi
67+
done
68+
69+
echo "Linear GraphQL did not return a valid JSON response after $attempts attempts." >&2
70+
exit 1
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
def finding_id($prefix):
2+
if ((.title // "") | startswith($prefix + " ")) then
3+
(.title | ltrimstr($prefix + " ") | split(": "))
4+
| if length > 1 then .[0] else "" end
5+
else
6+
""
7+
end;
8+
9+
[
10+
.data.issues.nodes[]?
11+
| select(finding_id($prefix) == $findingId)
12+
] as $matches
13+
| ($matches | map(select(.state.type != "completed" and .state.type != "canceled" and .state.type != "duplicate")) | .[0]) as $open
14+
| ($matches | map(select(.state.type == "completed" or .state.type == "canceled")) | .[0]) as $reopenable
15+
| ($open // $reopenable // $matches[0]) as $chosen
16+
| if $chosen == null then
17+
{
18+
linearIssueExists: false,
19+
linearIssueId: "",
20+
linearIssueIdentifier: "",
21+
linearIssueUrl: "",
22+
linearIssueClosed: false
23+
}
24+
else
25+
{
26+
linearIssueExists: true,
27+
linearIssueId: $chosen.id,
28+
linearIssueIdentifier: $chosen.identifier,
29+
linearIssueUrl: $chosen.url,
30+
linearIssueClosed: (($chosen.state.type == "completed") or ($chosen.state.type == "canceled"))
31+
}
32+
end

0 commit comments

Comments
 (0)