diff --git a/.farhoodlabs/.github/workflows/build-dev.yml b/.farhoodlabs/.github/workflows/build-dev.yml new file mode 100644 index 00000000..a483aae4 --- /dev/null +++ b/.farhoodlabs/.github/workflows/build-dev.yml @@ -0,0 +1,93 @@ +name: "Build: Dev" + +on: + push: + branches: [dev] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: runners-farhoodlabs + timeout-minutes: 30 + outputs: + image-tag: ${{ steps.tag.outputs.sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image tag + id: tag + run: echo "sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/farhoodlabs/paperclip-dev + tags: | + type=raw,value=latest + type=sha,prefix= + type=semver,pattern={{version}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: .farhoodlabs/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + no-cache: true + + update-infra: + needs: build + runs-on: runners-farhoodlabs + steps: + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PAPERCLIP_APP_ID }} + private-key: ${{ secrets.PAPERCLIP_APP_PRIVATE_KEY }} + repositories: paperclip-infra + + - name: Update dev image tag in infra repo + run: | + SHA="${{ needs.build.outputs.image-tag }}" + FILE="overlays/dev/kustomization.yaml" + + response=$(curl -sS \ + -H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/farhoodlabs/paperclip-infra/contents/$FILE") + + file_sha=$(echo "$response" | jq -r '.sha') + content=$(echo "$response" | jq -r '.content' | base64 -d) + new_content=$(echo "$content" | sed "s/newTag: \".*\"/newTag: \"$SHA\"/") + encoded=$(printf '%s' "$new_content" | base64 -w 0) + + curl -sS -X PUT \ + -H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/farhoodlabs/paperclip-infra/contents/$FILE" \ + -d "{\"message\":\"chore(cd): update paperclip-dev to $SHA\",\"content\":\"$encoded\",\"sha\":\"$file_sha\"}" diff --git a/.farhoodlabs/.github/workflows/build-prod.yml b/.farhoodlabs/.github/workflows/build-prod.yml new file mode 100644 index 00000000..8e187759 --- /dev/null +++ b/.farhoodlabs/.github/workflows/build-prod.yml @@ -0,0 +1,53 @@ +name: "Build: Production" + +on: + push: + branches: [local] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: runners-farhoodlabs + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/farhoodlabs/paperclip + tags: | + type=raw,value=latest + type=sha,prefix= + type=semver,pattern={{version}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + no-cache: true diff --git a/.farhoodlabs/CLAUDE.md b/.farhoodlabs/CLAUDE.md new file mode 100644 index 00000000..033ae480 --- /dev/null +++ b/.farhoodlabs/CLAUDE.md @@ -0,0 +1,80 @@ +# Paperclip Fork — Project Context + +This is a fork of [paperclipai/paperclip](https://github.com/paperclipai/paperclip). +Fork repo: https://github.com/farhoodlabs/paperclip + +## Branch Model + +| Branch | Purpose | +|---|---| +| `master` | Mirrors `upstream/master` exactly + `.farhoodlabs/` overlay directory + `assemble-local.yml` action. Never commit application code here. | +| `local` | **Default branch.** Assembled automatically by `assemble-local.yml` on every `master` push. Contains: upstream + fork Dockerfile/workflows + all pending upstream PR cherry-picks. Builds `ghcr.io/farhoodlabs/paperclip`. | +| `dev` | Development branch based on upstream/master. Builds `ghcr.io/farhoodlabs/paperclip-dev` on every push. | +| PR branches | `skill-pat-feature`, `skill-scan-refresh`, `feat/company-portability-complete`, `fix/far-108-k8s-adapter-reaper-liveness` — open PRs to upstream, never rebase onto master/local. | + +**Never commit directly to `local`** — it is fully regenerated by the assemble action and any direct commits will be overwritten. + +## Fork Overlay (`.farhoodlabs/`) + +Files committed to `master` that get copied into position on `local` by the assemble action: + +``` +.farhoodlabs/ + CLAUDE.md → CLAUDE.md (repo root) + Dockerfile → Dockerfile + .github/workflows/build-prod.yml → .github/workflows/build-prod.yml + .github/workflows/build-dev.yml → .github/workflows/build-dev.yml +``` + +The fork's Dockerfile production stage additions over upstream: `kubectl`, `kubeseal`, `uv`/`uvx`, `forgejo-cli` (`fj`, `fj-ex`, `fgj`), `nano`, `vim`. + +To modify fork-specific files, edit them in `.farhoodlabs/` on `master` and push — the assemble action will apply them to `local` automatically. + +## Pending Upstream PRs (included in `local`) + +These are cherry-picked/squashed onto `local` by the assemble action. When upstream merges one, remove its entry from `assemble-local.yml`. + +| PR | Branch | Method | Notes | +|---|---|---|---| +| #3237 | `skill-pat-feature` | cherry-pick | GitHub PAT support for private skill repos | +| #3351 | `skill-scan-refresh` | cherry-pick (exclude: skill-pat-feature) | Rebased onto skill-pat-feature | +| #3987 | `feat/company-portability-complete` | squash | Secrets export/import; squashed to bypass intra-PR merge commits | +| #4162 | `fix/far-108-k8s-adapter-reaper-liveness` | cherry-pick | K8s adapter reaper liveness | + +## Common Tasks + +### Sync upstream into master +```bash +git fetch upstream +git push origin upstream/master:master --force-with-lease +# assemble-local.yml triggers automatically and rebuilds local +``` + +### Add a new pending PR to local +Edit `.github/workflows/assemble-local.yml` on `master`: +- Simple PR (clean commits, no merge commits): add to `PR_CHERRY_PICK` +- Complex PR (has merge commits mixed in): add to `PR_SQUASH` +- If the branch was rebased onto another PR branch: use `exclude:base-branch` + +### Remove a PR after upstream merges it +Delete its entry from `PR_CHERRY_PICK` or `PR_SQUASH` in `assemble-local.yml` on `master`. + +### Submit a new PR to upstream +Branch from `upstream/master` (not from `local` or `master`): +```bash +git fetch upstream +git checkout -b feat/my-feature upstream/master +``` + +### Modify the fork Dockerfile +Edit `.farhoodlabs/Dockerfile` on `master`. Only modify the production stage — keep base/deps/build stages identical to upstream so diffs are minimal and upstream changes apply cleanly. + +## Deployment + +Paperclip runs in Kubernetes, not locally. Use `kubectl` to access it. The production image is `ghcr.io/farhoodlabs/paperclip:latest`. + +## Key Files + +- `.github/workflows/assemble-local.yml` — assembles `local` branch; edit this to manage pending PRs +- `.farhoodlabs/` — fork overlay; all fork-specific files live here on `master` +- `server/package.json` — has an adapter-utils workspace vs canary hack that needs fixing eventually diff --git a/.farhoodlabs/Dockerfile b/.farhoodlabs/Dockerfile new file mode 100644 index 00000000..c0570bc1 --- /dev/null +++ b/.farhoodlabs/Dockerfile @@ -0,0 +1,94 @@ +FROM node:lts-trixie-slim AS base +ARG USER_UID=1000 +ARG USER_GID=1000 +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates gosu curl gh git wget ripgrep python3 \ + && rm -rf /var/lib/apt/lists/* \ + && corepack enable + +# Modify the existing node user/group to have the specified UID/GID to match host user +RUN usermod -u $USER_UID --non-unique node \ + && groupmod -g $USER_GID --non-unique node \ + && usermod -g $USER_GID -d /paperclip node + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./ +COPY cli/package.json cli/ +COPY server/package.json server/ +COPY ui/package.json ui/ +COPY packages/shared/package.json packages/shared/ +COPY packages/db/package.json packages/db/ +COPY packages/adapter-utils/package.json packages/adapter-utils/ +COPY packages/mcp-server/package.json packages/mcp-server/ +COPY packages/adapters/claude-local/package.json packages/adapters/claude-local/ +COPY packages/adapters/codex-local/package.json packages/adapters/codex-local/ +COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/ +COPY packages/adapters/gemini-local/package.json packages/adapters/gemini-local/ +COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/ +COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/ +COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/ +COPY packages/plugins/sdk/package.json packages/plugins/sdk/ +COPY patches/ patches/ + +RUN pnpm install --frozen-lockfile + +FROM base AS build +WORKDIR /app +COPY --from=deps /app /app +COPY . . +RUN pnpm --filter @paperclipai/ui build +RUN pnpm --filter @paperclipai/plugin-sdk build +RUN pnpm --filter @paperclipai/server build +RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" && exit 1) + +FROM base AS production +ARG USER_UID=1000 +ARG USER_GID=1000 +WORKDIR /app +COPY --chown=node:node --from=build /app /app +# Fork additions: kubectl, kubeseal, uv, forgejo CLIs, editor tools +# Upstream installs: claude-code, codex, opencode-ai, openssh-client, jq +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssh-client jq nano vim \ + && rm -rf /var/lib/apt/lists/* \ + && curl -fsSL https://dl.k8s.io/release/v1.32.0/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl \ + && curl -fsSL https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.36.6/kubeseal-0.36.6-linux-amd64.tar.gz | tar -xzf - -C /tmp \ + && mv /tmp/kubeseal /usr/local/bin/kubeseal \ + && rm -rf /tmp/kubeseal /tmp/LICENSE /tmp/README.md \ + && curl -LsSf https://astral.sh/uv/install.sh | sh \ + && mv /root/.local/bin/uv /usr/local/bin/uv \ + && mv /root/.local/bin/uvx /usr/local/bin/uvx \ + && curl -fsSL https://codeberg.org/forgejo-contrib/forgejo-cli/releases/download/v0.4.1/forgejo-cli-linux.tar.gz | tar -xzf - -C /usr/local/bin \ + && chmod +x /usr/local/bin/fj \ + && curl -fsSL https://github.com/JKamsker/forgejo-cli-ex/releases/download/v0.1.7/fj-ex-linux-x86_64.tar.gz | tar -xzf - -C /usr/local/bin \ + && chmod +x /usr/local/bin/fj-ex \ + && curl -fsSL https://codeberg.org/romaintb/fgj/releases/download/v0.3.0/fgj_linux_amd64 -o /usr/local/bin/fgj \ + && chmod +x /usr/local/bin/fgj \ + && npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai \ + && mkdir -p /paperclip \ + && chown node:node /paperclip + +COPY scripts/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +ENV NODE_ENV=production \ + HOME=/paperclip \ + HOST=0.0.0.0 \ + PORT=3100 \ + SERVE_UI=true \ + PAPERCLIP_HOME=/paperclip \ + PAPERCLIP_INSTANCE_ID=default \ + USER_UID=${USER_UID} \ + USER_GID=${USER_GID} \ + PAPERCLIP_CONFIG=/paperclip/instances/default/config.json \ + PAPERCLIP_DEPLOYMENT_MODE=authenticated \ + PAPERCLIP_DEPLOYMENT_EXPOSURE=private \ + OPENCODE_ALLOW_ALL_MODELS=true + +VOLUME ["/paperclip"] +EXPOSE 3100 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["node", "--import", "./server/node_modules/tsx/dist/loader.mjs", "server/dist/index.js"] diff --git a/.github/workflows/assemble-local.yml b/.github/workflows/assemble-local.yml new file mode 100644 index 00000000..90f6de42 --- /dev/null +++ b/.github/workflows/assemble-local.yml @@ -0,0 +1,193 @@ +name: Assemble local branch + +# Triggers on every master push (i.e. after syncing upstream) and on demand. +# Builds the `local` branch: master + fork overlay + cherry-picked pending upstream PRs. +# Syncs build-dev.yml to the `dev` branch so every dev push triggers a build. +# +# PR entries support an optional "exclude:BRANCH" suffix to handle cases where +# one PR branch was rebased onto another. The exclude branch's commits are subtracted +# from the cherry-pick range so they aren't double-applied. +# +# When upstream merges a PR, remove its entry from PR_CHERRY_PICK or PR_SQUASH below. + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: write + actions: write + +jobs: + assemble: + runs-on: runners-farhoodlabs + timeout-minutes: 15 + steps: + - name: Checkout master + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all remotes + run: | + git remote add upstream https://github.com/paperclipai/paperclip.git 2>/dev/null || true + git fetch --all --quiet + + - name: Assemble local branch + run: | + set -euo pipefail + + # Start local from master (which mirrors upstream) + git checkout -B local origin/master + + # Apply fork overlay: Dockerfile, build workflows, CLAUDE.md + cp .farhoodlabs/Dockerfile Dockerfile + cp .farhoodlabs/CLAUDE.md CLAUDE.md + mkdir -p .github/workflows + cp .farhoodlabs/.github/workflows/build-prod.yml .github/workflows/build-prod.yml + cp .farhoodlabs/.github/workflows/build-dev.yml .github/workflows/build-dev.yml + git add Dockerfile CLAUDE.md .github/workflows/build-prod.yml .github/workflows/build-dev.yml + git commit -m "chore: apply fork overlay from .farhoodlabs" + + # --- PRs to cherry-pick commit-by-commit (clean, no merge commits) --- + # Format: "PR-number branch-name [exclude:base-branch]" + # Use exclude: when a branch was rebased onto another PR branch to avoid double-applying commits. + # Remove an entry here when upstream merges the PR. + PR_CHERRY_PICK=( + "3237 skill-pat-feature" + "3351 skill-scan-refresh exclude:skill-pat-feature" + "4162 fix/far-108-k8s-adapter-reaper-liveness" + ) + + for entry in "${PR_CHERRY_PICK[@]}"; do + # Parse: pr_num, branch, optional exclude branch + pr_num=$(echo "$entry" | awk '{print $1}') + branch=$(echo "$entry" | awk '{print $2}') + exclude_branch=$(echo "$entry" | grep -oP '(?<=exclude:)\S+' || true) + remote_branch="origin/$branch" + exclude_arg="" + if [ -n "$exclude_branch" ]; then + exclude_arg="--not origin/$exclude_branch" + fi + + if ! git rev-parse "$remote_branch" &>/dev/null; then + echo "WARNING: $remote_branch not found, skipping PR #$pr_num" + continue + fi + + # Exclude commits already on origin/master (fork-overlay/CI infra + # that landed on master via the .farhoodlabs/ overlay path). PR + # branches sometimes pull these in via `git merge origin/master`, + # but cherry-picking them onto `local` (which is already master) + # is redundant and produces conflicts on the assemble-local file. + mapfile -t commits < <(git log --no-merges --reverse --format="%H" upstream/master.."$remote_branch" ^origin/master $exclude_arg) + + if [ ${#commits[@]} -eq 0 ]; then + echo "PR #$pr_num ($branch): no unique commits — likely merged upstream, skipping" + continue + fi + + echo "PR #$pr_num ($branch): cherry-picking ${#commits[@]} commit(s)" + for sha in "${commits[@]}"; do + git cherry-pick "$sha" || { + # If the cherry-pick produced an empty result (commit's content + # is already in HEAD via auto-merge), skip it instead of failing. + # State signature: CHERRY_PICK_HEAD set, no unmerged paths, + # nothing staged. + if [ -f .git/CHERRY_PICK_HEAD ] \ + && [ -z "$(git diff --name-only --diff-filter=U)" ] \ + && git diff --staged --quiet; then + echo "PR #$pr_num: $sha became empty after merge, skipping" + git cherry-pick --skip + continue + fi + echo "::error::Cherry-pick conflict at $sha from PR #$pr_num ($branch)" + echo "::error::Resolve the conflict, force-push the branch, then re-run this workflow" + git cherry-pick --abort + exit 1 + } + done + done + + # --- PRs to apply as a single squash (complex history with merge commits) --- + # git merge --squash applies the net final diff of the branch, bypassing + # intra-PR commit ordering issues. CI commits that cancel out are ignored. + # Remove an entry here when upstream merges the PR. + PR_SQUASH=( + "3987 feat/company-portability-complete" + ) + + for entry in "${PR_SQUASH[@]}"; do + pr_num="${entry%% *}" + branch="${entry#* }" + remote_branch="origin/$branch" + + if ! git rev-parse "$remote_branch" &>/dev/null; then + echo "WARNING: $remote_branch not found, skipping PR #$pr_num" + continue + fi + + # Check if the branch has any unique non-merge commits + unique=$(git log --no-merges --oneline upstream/master.."$remote_branch" | wc -l) + if [ "$unique" -eq 0 ]; then + echo "PR #$pr_num ($branch): no unique commits — likely merged upstream, skipping" + continue + fi + + echo "PR #$pr_num ($branch): applying as squash ($unique non-merge commits)" + git merge --squash "$remote_branch" || { + echo "::error::Squash conflict for PR #$pr_num ($branch)" + git merge --abort 2>/dev/null || git reset --hard HEAD + exit 1 + } + # Only commit if there are staged changes + git diff --staged --quiet || git commit -m "feat: apply PR #$pr_num ($branch)" + done + + git push origin local --force + echo "local branch assembled and pushed" + + - name: Trigger prod build + run: | + curl -sS -X POST \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-prod.yml/dispatches \ + -d '{"ref":"local"}' + + - name: Sync build-dev.yml to dev branch + run: | + set -euo pipefail + + if ! git rev-parse origin/dev &>/dev/null; then + echo "dev branch not found on origin, skipping" + exit 0 + fi + + canonical=".farhoodlabs/.github/workflows/build-dev.yml" + target=".github/workflows/build-dev.yml" + + if git show origin/dev:"$target" 2>/dev/null | diff --brief - "$canonical" &>/dev/null; then + echo "build-dev.yml on dev is up to date, skipping" + exit 0 + fi + + echo "Syncing build-dev.yml to dev branch..." + # Save canonical content before switching branches (.farhoodlabs/ only exists on master) + tmp=$(mktemp) + cp "$canonical" "$tmp" + git checkout -B dev-wf-sync origin/dev + mkdir -p "$(dirname "$target")" + cp "$tmp" "$target" + rm "$tmp" + git add "$target" + git commit -m "chore(ci): sync build-dev.yml from .farhoodlabs" + git push origin dev-wf-sync:dev + echo "build-dev.yml synced to dev" diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml new file mode 100644 index 00000000..a483aae4 --- /dev/null +++ b/.github/workflows/build-dev.yml @@ -0,0 +1,93 @@ +name: "Build: Dev" + +on: + push: + branches: [dev] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: runners-farhoodlabs + timeout-minutes: 30 + outputs: + image-tag: ${{ steps.tag.outputs.sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image tag + id: tag + run: echo "sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/farhoodlabs/paperclip-dev + tags: | + type=raw,value=latest + type=sha,prefix= + type=semver,pattern={{version}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: .farhoodlabs/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + no-cache: true + + update-infra: + needs: build + runs-on: runners-farhoodlabs + steps: + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PAPERCLIP_APP_ID }} + private-key: ${{ secrets.PAPERCLIP_APP_PRIVATE_KEY }} + repositories: paperclip-infra + + - name: Update dev image tag in infra repo + run: | + SHA="${{ needs.build.outputs.image-tag }}" + FILE="overlays/dev/kustomization.yaml" + + response=$(curl -sS \ + -H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/farhoodlabs/paperclip-infra/contents/$FILE") + + file_sha=$(echo "$response" | jq -r '.sha') + content=$(echo "$response" | jq -r '.content' | base64 -d) + new_content=$(echo "$content" | sed "s/newTag: \".*\"/newTag: \"$SHA\"/") + encoded=$(printf '%s' "$new_content" | base64 -w 0) + + curl -sS -X PUT \ + -H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/farhoodlabs/paperclip-infra/contents/$FILE" \ + -d "{\"message\":\"chore(cd): update paperclip-dev to $SHA\",\"content\":\"$encoded\",\"sha\":\"$file_sha\"}" diff --git a/.github/workflows/build-prod.yml b/.github/workflows/build-prod.yml new file mode 100644 index 00000000..8e187759 --- /dev/null +++ b/.github/workflows/build-prod.yml @@ -0,0 +1,53 @@ +name: "Build: Production" + +on: + push: + branches: [local] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: runners-farhoodlabs + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/farhoodlabs/paperclip + tags: | + type=raw,value=latest + type=sha,prefix= + type=semver,pattern={{version}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + no-cache: true diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 490290c2..05ab6e3c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,55 +1,19 @@ +# Disabled in fork — Docker builds are handled by the fork overlay: +# build-prod.yml triggers on `local` branch → ghcr.io/farhoodlabs/paperclip +# build-dev.yml triggers on `dev` branch → ghcr.io/farhoodlabs/paperclip-dev +# See .farhoodlabs/.github/workflows/ and .github/workflows/assemble-local.yml +# +# NOTE: upstream may overwrite this file when master is synced. Re-apply if that happens, +# or use the sync-upstream.yml action which re-applies these overrides automatically. name: Docker - on: - push: - branches: - - "master" - tags: - - "v*" - -permissions: - contents: read - packages: write - + workflow_dispatch: + inputs: + note: + description: "Disabled in fork. Use build-prod.yml (local) or build-dev.yml (dev)." + required: false jobs: - build-and-push: + disabled: runs-on: ubuntu-latest - timeout-minutes: 30 - concurrency: - group: docker-${{ github.ref }} - cancel-in-progress: true steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - cache-from: type=gha - cache-to: type=gha,mode=max - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + - run: echo "Disabled. See build-prod.yml and build-dev.yml." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b5983cc..335933b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,261 +1,16 @@ +# Disabled in fork — package publishing is not applicable to this fork. +# +# NOTE: upstream may overwrite this file when master is synced. Re-apply if that happens, +# or use the sync-upstream.yml action which re-applies these overrides automatically. name: Release - on: - push: - branches: - - master workflow_dispatch: inputs: - source_ref: - description: Commit SHA, branch, or tag to publish as stable - required: true - type: string - default: master - stable_date: - description: Enter a UTC date in YYYY-MM-DD format, for example 2026-03-18. Do not enter a version string. The workflow will resolve that date to a stable version such as 2026.318.0, then 2026.318.1 for the next same-day stable. + note: + description: "Disabled in fork." required: false - type: string - dry_run: - description: Preview the stable release without publishing - required: true - type: boolean - default: false - -concurrency: - group: release-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: false - jobs: - verify_canary: - if: github.event_name == 'push' + disabled: runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Typecheck - run: pnpm -r typecheck - - - name: Run tests - run: pnpm test:run - - - name: Build - run: pnpm build - - publish_canary: - if: github.event_name == 'push' - needs: verify_canary - runs-on: ubuntu-latest - timeout-minutes: 45 - environment: npm-canary - permissions: - contents: write - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Restore tracked install-time changes - run: git checkout -- pnpm-lock.yaml - - - name: Configure git author - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Publish canary - env: - GITHUB_ACTIONS: "true" - run: ./scripts/release.sh canary --skip-verify - - - name: Push canary tag - run: | - tag="$(git tag --points-at HEAD | grep '^canary/v' | head -1)" - if [ -z "$tag" ]; then - echo "Error: no canary tag points at HEAD after release." >&2 - exit 1 - fi - git push origin "refs/tags/${tag}" - - verify_stable: - if: github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.source_ref }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Typecheck - run: pnpm -r typecheck - - - name: Run tests - run: pnpm test:run - - - name: Build - run: pnpm build - - preview_stable: - if: github.event_name == 'workflow_dispatch' && inputs.dry_run - needs: verify_stable - runs-on: ubuntu-latest - timeout-minutes: 45 - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.source_ref }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Dry-run stable release - env: - GITHUB_ACTIONS: "true" - run: | - args=(stable --skip-verify --dry-run) - if [ -n "${{ inputs.stable_date }}" ]; then - args+=(--date "${{ inputs.stable_date }}") - fi - ./scripts/release.sh "${args[@]}" - - publish_stable: - if: github.event_name == 'workflow_dispatch' && !inputs.dry_run - needs: verify_stable - runs-on: ubuntu-latest - timeout-minutes: 45 - environment: npm-stable - permissions: - contents: write - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.source_ref }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Restore tracked install-time changes - run: git checkout -- pnpm-lock.yaml - - - name: Configure git author - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Publish stable - env: - GITHUB_ACTIONS: "true" - run: | - args=(stable --skip-verify) - if [ -n "${{ inputs.stable_date }}" ]; then - args+=(--date "${{ inputs.stable_date }}") - fi - ./scripts/release.sh "${args[@]}" - - - name: Push stable tag - run: | - tag="$(git tag --points-at HEAD | grep '^v' | head -1)" - if [ -z "$tag" ]; then - echo "Error: no stable tag points at HEAD after release." >&2 - exit 1 - fi - git push origin "refs/tags/${tag}" - - - name: Create GitHub Release - env: - GH_TOKEN: ${{ github.token }} - PUBLISH_REMOTE: origin - run: | - version="$(git tag --points-at HEAD | grep '^v' | head -1 | sed 's/^v//')" - if [ -z "$version" ]; then - echo "Error: no v* tag points at HEAD after stable release." >&2 - exit 1 - fi - ./scripts/create-github-release.sh "$version" + - run: echo "Disabled in fork." diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000..20ae3c19 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,70 @@ +name: Sync upstream + +# Syncs upstream/master into this fork's master, then re-applies fork overrides +# for any upstream workflow files that should not run in the fork (docker.yml, release.yml). +# Triggers assemble-local.yml automatically via the master push. +# +# Run manually or on a schedule to keep master current with upstream. + +on: + schedule: + - cron: '0 7 * * *' # daily at 2am EST (UTC-5) + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync: + runs-on: runners-farhoodlabs + timeout-minutes: 10 + steps: + - name: Checkout master + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch upstream + run: | + git remote add upstream https://github.com/paperclipai/paperclip.git 2>/dev/null || true + git fetch upstream + + - name: Fast-forward master to upstream + run: | + git merge --ff-only upstream/master || { + echo "::error::Cannot fast-forward master to upstream/master — diverged history" + echo "::error::Resolve manually: git fetch upstream && git rebase upstream/master" + exit 1 + } + + - name: Re-apply fork workflow overrides + run: | + # These files are overridden in the fork to prevent upstream workflows from + # running on fork pushes. Re-apply after each upstream sync. + OVERRIDE_FILES=( + ".github/workflows/docker.yml" + ".github/workflows/release.yml" + ) + changed=false + for f in "${OVERRIDE_FILES[@]}"; do + fork_version=$(git show origin/master:"$f" 2>/dev/null || true) + current=$(cat "$f" 2>/dev/null || true) + if [ "$fork_version" != "$current" ]; then + echo "Re-applying fork override: $f" + git checkout origin/master -- "$f" + changed=true + fi + done + if [ "$changed" = true ]; then + git add "${OVERRIDE_FILES[@]}" + git commit -m "chore(ci): re-apply fork workflow overrides after upstream sync" + fi + + - name: Push master + run: git push origin master diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..033ae480 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# Paperclip Fork — Project Context + +This is a fork of [paperclipai/paperclip](https://github.com/paperclipai/paperclip). +Fork repo: https://github.com/farhoodlabs/paperclip + +## Branch Model + +| Branch | Purpose | +|---|---| +| `master` | Mirrors `upstream/master` exactly + `.farhoodlabs/` overlay directory + `assemble-local.yml` action. Never commit application code here. | +| `local` | **Default branch.** Assembled automatically by `assemble-local.yml` on every `master` push. Contains: upstream + fork Dockerfile/workflows + all pending upstream PR cherry-picks. Builds `ghcr.io/farhoodlabs/paperclip`. | +| `dev` | Development branch based on upstream/master. Builds `ghcr.io/farhoodlabs/paperclip-dev` on every push. | +| PR branches | `skill-pat-feature`, `skill-scan-refresh`, `feat/company-portability-complete`, `fix/far-108-k8s-adapter-reaper-liveness` — open PRs to upstream, never rebase onto master/local. | + +**Never commit directly to `local`** — it is fully regenerated by the assemble action and any direct commits will be overwritten. + +## Fork Overlay (`.farhoodlabs/`) + +Files committed to `master` that get copied into position on `local` by the assemble action: + +``` +.farhoodlabs/ + CLAUDE.md → CLAUDE.md (repo root) + Dockerfile → Dockerfile + .github/workflows/build-prod.yml → .github/workflows/build-prod.yml + .github/workflows/build-dev.yml → .github/workflows/build-dev.yml +``` + +The fork's Dockerfile production stage additions over upstream: `kubectl`, `kubeseal`, `uv`/`uvx`, `forgejo-cli` (`fj`, `fj-ex`, `fgj`), `nano`, `vim`. + +To modify fork-specific files, edit them in `.farhoodlabs/` on `master` and push — the assemble action will apply them to `local` automatically. + +## Pending Upstream PRs (included in `local`) + +These are cherry-picked/squashed onto `local` by the assemble action. When upstream merges one, remove its entry from `assemble-local.yml`. + +| PR | Branch | Method | Notes | +|---|---|---|---| +| #3237 | `skill-pat-feature` | cherry-pick | GitHub PAT support for private skill repos | +| #3351 | `skill-scan-refresh` | cherry-pick (exclude: skill-pat-feature) | Rebased onto skill-pat-feature | +| #3987 | `feat/company-portability-complete` | squash | Secrets export/import; squashed to bypass intra-PR merge commits | +| #4162 | `fix/far-108-k8s-adapter-reaper-liveness` | cherry-pick | K8s adapter reaper liveness | + +## Common Tasks + +### Sync upstream into master +```bash +git fetch upstream +git push origin upstream/master:master --force-with-lease +# assemble-local.yml triggers automatically and rebuilds local +``` + +### Add a new pending PR to local +Edit `.github/workflows/assemble-local.yml` on `master`: +- Simple PR (clean commits, no merge commits): add to `PR_CHERRY_PICK` +- Complex PR (has merge commits mixed in): add to `PR_SQUASH` +- If the branch was rebased onto another PR branch: use `exclude:base-branch` + +### Remove a PR after upstream merges it +Delete its entry from `PR_CHERRY_PICK` or `PR_SQUASH` in `assemble-local.yml` on `master`. + +### Submit a new PR to upstream +Branch from `upstream/master` (not from `local` or `master`): +```bash +git fetch upstream +git checkout -b feat/my-feature upstream/master +``` + +### Modify the fork Dockerfile +Edit `.farhoodlabs/Dockerfile` on `master`. Only modify the production stage — keep base/deps/build stages identical to upstream so diffs are minimal and upstream changes apply cleanly. + +## Deployment + +Paperclip runs in Kubernetes, not locally. Use `kubectl` to access it. The production image is `ghcr.io/farhoodlabs/paperclip:latest`. + +## Key Files + +- `.github/workflows/assemble-local.yml` — assembles `local` branch; edit this to manage pending PRs +- `.farhoodlabs/` — fork overlay; all fork-specific files live here on `master` +- `server/package.json` — has an adapter-utils workspace vs canary hack that needs fixing eventually