Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 69 additions & 0 deletions .github/actions/open-or-update-pr/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Open or update pull request
description: >-
Commits the working-tree changes under `add-paths` to `branch`, pushes, and
opens or updates a pull request onto `base`. With no changes left, closes the
open pull request from `branch` (if any) and deletes the branch. If someone
other than the bot pushed to `branch`, leaves it alone and says so on the pull
request, so a reviewer's fix is never overwritten. Local replacement for
peter-evans/create-pull-request, which the org's Actions policy does not allow.

inputs:
token:
description: GitHub App installation token with contents and pull-requests write
required: true
bot-slug:
description: App slug, used as the commit author `<slug>[bot]`
required: true
path:
description: Repository checkout to operate on, relative to the workspace
required: false
default: "."
add-paths:
description: Newline-separated git pathspecs to commit
required: true
branch:
description: Pull request branch (force-pushed)
required: true
base:
description: Pull request base branch
required: false
default: main
title:
description: Pull request title
required: true
body-path:
description: File containing the pull request body
required: true
commit-message:
description: Commit message
required: true
labels:
description: Comma-separated labels for the pull request (created if missing)
required: false
default: ""

outputs:
operation:
description: created | updated | unchanged | closed | none | held
value: ${{ steps.pr.outputs.operation }}
url:
description: Pull request URL, when one exists
value: ${{ steps.pr.outputs.url }}

runs:
using: composite
steps:
- id: pr
shell: bash
working-directory: ${{ inputs.path }}
env:
GH_TOKEN: ${{ inputs.token }}
BOT_SLUG: ${{ inputs.bot-slug }}
ADD_PATHS: ${{ inputs.add-paths }}
BRANCH: ${{ inputs.branch }}
BASE: ${{ inputs.base }}
TITLE: ${{ inputs.title }}
BODY_PATH: ${{ inputs.body-path }}
COMMIT_MESSAGE: ${{ inputs.commit-message }}
LABELS: ${{ inputs.labels }}
run: bash "$GITHUB_ACTION_PATH/open-or-update-pr.sh"
101 changes: 101 additions & 0 deletions .github/actions/open-or-update-pr/open-or-update-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# See action.yml. Runs inside the target repository checkout.
set -euo pipefail

out() { echo "$1=$2" >> "$GITHUB_OUTPUT"; echo "$1: $2"; }

# Pushes and fetches use the App token, not whatever actions/checkout persisted.
b64_auth="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
git config --unset-all http.https://github.com/.extraheader 2>/dev/null || true
authed() { git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${b64_auth}" "$@"; }

# Stage exactly the requested paths (deletions included).
paths=()
while IFS= read -r p; do [ -n "$p" ] && paths+=("$p"); done <<< "$ADD_PATHS"
git add -A -- "${paths[@]}"

# Exit 2 means no such branch; anything else non-zero is a real failure.
remote_sha="$(authed ls-remote --exit-code --heads origin "$BRANCH" | cut -f1)" || {
status=$?
[ "$status" -eq 2 ] || { echo "::error::Couldn't read ${BRANCH} from origin (exit ${status})"; exit 1; }
remote_sha=""
}
[ -n "$remote_sha" ] && authed fetch -q origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" "+refs/heads/${BASE}:refs/remotes/origin/${BASE}"
# Only this repo's own branch: a fork PR can have a branch of the same name.
owner="$(gh repo view --json owner --jq .owner.login)"
pr_number="$(gh pr list --head "$BRANCH" --base "$BASE" --state open --json number,headRepositoryOwner \
--jq "[.[] | select(.headRepositoryOwner.login == \"${owner}\")][0].number // empty")"

# A commit someone else wrote, or rewrote, would be lost to the force-push or the branch
# delete below, so hold off and say so once on the PR. Rebuilding resumes when the PR is
# merged or closed.
held_marker="<!-- open-or-update-pr: held -->"
if [ -n "$remote_sha" ]; then
others="$(git log --format='%an%n%cn' "refs/remotes/origin/${BASE}..refs/remotes/origin/${BRANCH}" | grep -vxF "${BOT_SLUG}[bot]" || true)"
if [ -n "$others" ] && [ -z "$pr_number" ]; then
# No open PR: if the branch is exactly where a merged or closed PR left it, GitHub keeps
# those commits with that PR, so start over. Anything pushed since is someone's work.
ended_with="$(gh pr list --head "$BRANCH" --base "$BASE" --state all --json headRefOid,headRepositoryOwner \
--jq ".[] | select(.headRepositoryOwner.login == \"${owner}\") | .headRefOid")"
grep -qx "$remote_sha" <<< "$ended_with" || { echo "::error::${BRANCH} has commits by someone other than the bot that no PR ended with; leaving it alone"; exit 1; }
authed push -q "--force-with-lease=refs/heads/${BRANCH}:${remote_sha}" origin --delete "$BRANCH"
remote_sha=""
others=""
fi
if [ -n "$others" ]; then
if [ -n "$pr_number" ] && ! gh pr view "$pr_number" --json comments --jq '.comments[].body' | grep -qF "$held_marker"; then
gh pr comment "$pr_number" --body "Someone other than the bot pushed to this branch, so the daily run no longer updates it. Merge or close this PR to let the next run pick up newer changes.
${held_marker}"
fi
out operation held
out url "$([ -n "$pr_number" ] && gh pr view "$pr_number" --json url --jq .url || true)"
exit 0
fi
fi

if git diff --cached --quiet; then
if [ -n "$pr_number" ]; then
gh pr close "$pr_number" --comment "Closing: \`${BASE}\` no longer differs from the source, so this change is no longer needed."
out operation closed
out url "$(gh pr view "$pr_number" --json url --jq .url)"
else
out operation none
out url ""
fi
[ -n "$remote_sha" ] && authed push -q "--force-with-lease=refs/heads/${BRANCH}:${remote_sha}" origin --delete "$BRANCH"
exit 0
fi

if [ -n "$remote_sha" ] && [ -n "$pr_number" ] && git diff --cached --quiet "refs/remotes/origin/${BRANCH}" --; then
out operation unchanged
out url "$(gh pr view "$pr_number" --json url --jq .url)"
exit 0
fi

# Build the commit from the index without moving HEAD or touching any local branch.
bot_id="$(gh api "/users/${BOT_SLUG}%5Bbot%5D" --jq .id)"
export GIT_AUTHOR_NAME="${BOT_SLUG}[bot]" GIT_COMMITTER_NAME="${BOT_SLUG}[bot]"
export GIT_AUTHOR_EMAIL="${bot_id}+${BOT_SLUG}[bot]@users.noreply.github.com" GIT_COMMITTER_EMAIL="${bot_id}+${BOT_SLUG}[bot]@users.noreply.github.com"
commit="$(git commit-tree "$(git write-tree)" -p HEAD -m "$COMMIT_MESSAGE")"
# The lease fails the push if anyone pushed since the check above.
authed push -q "--force-with-lease=refs/heads/${BRANCH}:${remote_sha}" origin "${commit}:refs/heads/${BRANCH}"

label_args=()
IFS=',' read -ra labels <<< "$LABELS"
for label in ${labels[@]+"${labels[@]}"}; do
label="$(echo "$label" | xargs)"
[ -z "$label" ] && continue
gh label create "$label" >/dev/null 2>&1 || true # already exists
label_args+=(--add-label "$label")
done

if [ -n "$pr_number" ]; then
gh pr edit "$pr_number" --title "$TITLE" --body-file "$BODY_PATH" ${label_args[@]+"${label_args[@]}"} >/dev/null
out operation updated
out url "$(gh pr view "$pr_number" --json url --jq .url)"
else
url="$(gh pr create --head "$BRANCH" --base "$BASE" --title "$TITLE" --body-file "$BODY_PATH")"
[ ${#label_args[@]} -gt 0 ] && gh pr edit "$url" ${label_args[@]+"${label_args[@]}"} >/dev/null
out operation created
out url "$url"
fi
146 changes: 146 additions & 0 deletions .github/workflows/drift-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: SDK drift

# Drift checks 1-3: keeps the SDK's copies of apper values in step with apper, one rolling
# PR per check on bot/drift/<check>. extract_apper.py reads the apper files; sync-sdk.mjs
# writes the SDK file. When a PR merges, publish-sdk-change-to-docs.yml opens the docs PR.
# Design: "Drift check automation design" (Google Doc).
#
# The docs bot's key reads apper, so it lives in the `drift` environment, which only main
# can use: a branch in this public repo can't run a changed workflow against it. Nothing
# from apper is printed except the values the SDK publishes anyway.

on:
schedule:
- cron: "30 5 * * *" # Daily, 05:30 UTC
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
sync:
name: ${{ matrix.check }}
runs-on: ubuntu-latest
environment: drift
strategy:
fail-fast: false
matrix:
include:
- check: models
apper_files: backend/app/integrations/core/runtime_model_config.py
target: packages/sdk/src/modules/integrations.types.ts
title: "fix(types): sync InvokeLLM models with apper"
- check: tool-status
apper_files: backend/app/ai/models/message.py
target: packages/sdk/src/modules/agents.types.ts
title: "fix(types): sync agent tool call statuses with apper"
- check: connectors
apper_files: >-
backend/app/external_auth/integrations/config/connectors.json
backend/app/external_auth/models/constants.py
target: packages/sdk/src/modules/connectors.types.ts
title: "docs(connectors): sync the Available connectors table with apper"
concurrency:
group: sdk-drift-${{ matrix.check }}
cancel-in-progress: false

steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0

- name: Wix gateway proxy (mandatory)
uses: ./.github/actions/wix-gateway-proxy

- name: apper read token
id: apper-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
with:
app-id: ${{ vars.DOCS_BOT_APP_ID }}
private-key: ${{ secrets.DOCS_BOT_PRIVATE_KEY }}
owner: base44-dev
repositories: apper
permission-contents: read

- name: Fetch the apper files
env:
GH_TOKEN: ${{ steps.apper-token.outputs.token }}
FILES: ${{ matrix.apper_files }}
run: |
set -euo pipefail
for rel in $FILES; do
mkdir -p "$RUNNER_TEMP/apper/$(dirname "$rel")"
gh api -H "Accept: application/vnd.github.raw+json" "/repos/base44-dev/apper/contents/$rel?ref=main" > "$RUNNER_TEMP/apper/$rel"
test -s "$RUNNER_TEMP/apper/$rel" || { echo "::error::Fetched an empty $rel"; exit 1; }
done

- name: Sync from apper
env:
CHECK: ${{ matrix.check }}
run: |
set -euo pipefail
python3 packages/sdk/scripts/drift/extract_apper.py --apper "$RUNNER_TEMP/apper" "$CHECK" > "$RUNNER_TEMP/extract.json"
node packages/sdk/scripts/drift/sync-sdk.mjs --check "$CHECK" --source "$RUNNER_TEMP/extract.json" --summary "$RUNNER_TEMP/summary.md"
{
cat "$RUNNER_TEMP/summary.md"
echo
echo "_Opened by the SDK drift workflow. It runs daily and updates this PR while the drift lasts. When it merges, the docs PR opens in mintlify-docs and waits for the next SDK release._"
} > "$RUNNER_TEMP/pr-body.md"

- name: javascript-sdk write token
id: sdk-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
with:
app-id: ${{ vars.BASE44_GITHUB_ACTIONS_APP_ID }}
private-key: ${{ secrets.BASE44_GITHUB_ACTIONS_APP_PRIVATE_KEY }}
owner: base44
repositories: javascript-sdk
permission-contents: write
permission-pull-requests: write

- name: Open or update the pull request
id: pr
uses: ./.github/actions/open-or-update-pr
with:
token: ${{ steps.sdk-token.outputs.token }}
bot-slug: ${{ steps.sdk-token.outputs.app-slug }}
add-paths: ${{ matrix.target }}
branch: bot/drift/${{ matrix.check }}
base: main
title: ${{ matrix.title }}
body-path: ${{ runner.temp }}/pr-body.md
commit-message: ${{ matrix.title }}
labels: drift

- name: Report
env:
OPERATION: ${{ steps.pr.outputs.operation }}
URL: ${{ steps.pr.outputs.url }}
run: |
{
echo "## ${{ matrix.check }}"
echo
echo "Pull request: \`${OPERATION:-none}\` ${URL}"
echo
cat "$RUNNER_TEMP/summary.md"
} >> "$GITHUB_STEP_SUMMARY"

- name: Open or comment on the failure issue
if: ${{ failure() }}
env:
GH_TOKEN: ${{ github.token }}
CHECK: ${{ matrix.check }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
title="SDK drift check failing: $CHECK"
gh label create drift --color 0E8A16 --description "Opened by the docs drift workflows" --force
issue=$(gh issue list --state open --label drift --json number,title --jq ".[] | select(.title == \"$title\") | .number" | head -1)
if [ -n "$issue" ]; then
gh issue comment "$issue" --body "Failed again: $RUN_URL"
else
gh issue create --title "$title" --label drift --body "The daily SDK drift check \`$CHECK\` failed: $RUN_URL

Close this issue once it's fixed; the next failure opens a new one."
fi
Loading
Loading