From 2f4e3192839273769c38d7dde5225d86d6d0495f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Jul 2026 16:23:59 +0000 Subject: [PATCH 01/10] ci: add notify parent workflow --- .github/workflows/notify-parent.yml | 104 ++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .github/workflows/notify-parent.yml diff --git a/.github/workflows/notify-parent.yml b/.github/workflows/notify-parent.yml new file mode 100644 index 000000000..186d5a4d0 --- /dev/null +++ b/.github/workflows/notify-parent.yml @@ -0,0 +1,104 @@ +name: Notify parent to bump submodule + +on: + push: + branches: [ main ] # <- Edit here to tracked branch + workflow_dispatch: {} + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Prepare payload + id: prep + run: | + echo "sha=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT" + + - name: Dispatch to parent (FuturHub) # <- Edit here + env: + GH_PARENT_REPO: FuturHandRobotics/FuturHub # <- Edit here + GH_TOKEN: ${{ secrets.FUTURHAND_SUBMODULE_UPDATE }} + SUBMODULE_PATH_IN_PARENT: external/foundationpose_grpc # <- Edit here + SHA: # Empty defaults to latest + REPO: ${{ steps.prep.outputs.repo }} + run: | + set -Eeuo pipefail + + # Normalize PAT: strip CR/LF so gh can use it in Authorization header + export GH_TOKEN="$(printf %s "$GH_TOKEN" | tr -d '\r\n')" + + payload=$(printf '{"event_type":"submodule_update","client_payload":{"submodule_path":"%s","sha":"%s","repo":"%s"}}' \ + "$SUBMODULE_PATH_IN_PARENT" "$SHA" "$REPO") + + echo "::group::Dispatch payload" + echo "$payload" + echo "::endgroup::" + + # Send request and capture body + status code (gh api → reliable JSON dispatch) + echo "$payload" > payload.json + resp_file="$(mktemp)" + gh api "repos/${GH_PARENT_REPO}/dispatches" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + --input payload.json \ + -X POST \ + -i > "$resp_file" + code="$(awk 'NR==1{print $2}' "$resp_file")" + + echo "::group::Response (HTTP $code)" + # Body may be empty on success (204); still show for transparency + if [ -s "$resp_file" ]; then cat "$resp_file"; else echo ""; fi + echo "::endgroup::" + + case "$code" in + 204) + echo "::notice title=Repository dispatch accepted::Parent=${GH_PARENT_REPO} path=${SUBMODULE_PATH_IN_PARENT} sha=${SHA}" + ;; + 401|403) + echo "::error title=Auth/Scope issue::HTTP $code. The PAT likely lacks access or is expired. + - Ensure the token is a **fine-grained PAT** granted to **${GH_PARENT_REPO}** + - Repo permissions: **Contents: Read and write** + - Secret name is correct in this repo + - Token not expired / revoked + See response above for details." + exit 1 + ;; + 404) + echo "::error title=Not found / access denied::HTTP 404. Check: + - GH_PARENT_REPO='${GH_PARENT_REPO}' is correct (owner/repo) + - PAT has access to that repo (same org/owner, correct repo selection) + - Repo is not private to a different owner without permission" + exit 1 + ;; + 422|400) + echo "::error title=Unprocessable payload::HTTP $code. Likely JSON shape, event_type, or required fields. + - event_type should match parent workflow: 'submodule_update' + - client_payload must include: submodule_path, sha, repo + - Validate quotes/escaping in payload (shown above)" + exit 1 + ;; + 5*) + echo "::warning title=GitHub server error::HTTP $code. Transient issue—consider retry/backoff." + + # Optional quick retry (1x): + sleep 2 + gh api "repos/${GH_PARENT_REPO}/dispatches" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + --input payload.json \ + -X POST \ + -i > "$resp_file" + code2="$(awk 'NR==1{print $2}' "$resp_file")" + + echo "::notice::Retry status: $code2" + if [ "$code2" != "204" ]; then + echo "::error::Retry failed. See response above." + exit 1 + fi + ;; + *) + echo "::error title=Unexpected status::HTTP $code. Inspect response above. Visit: https://docs.github.com/rest" + exit 1 + ;; + esac From b13ced096bf518f339b8ff820321c6b1fca96eda Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Jul 2026 17:41:29 +0000 Subject: [PATCH 02/10] ci: add notify parent workflow --- .github/workflows/notify-parent.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/notify-parent.yml b/.github/workflows/notify-parent.yml index 186d5a4d0..fd39fc216 100644 --- a/.github/workflows/notify-parent.yml +++ b/.github/workflows/notify-parent.yml @@ -19,7 +19,7 @@ jobs: env: GH_PARENT_REPO: FuturHandRobotics/FuturHub # <- Edit here GH_TOKEN: ${{ secrets.FUTURHAND_SUBMODULE_UPDATE }} - SUBMODULE_PATH_IN_PARENT: external/foundationpose_grpc # <- Edit here + SUBMODULE_PATH_IN_PARENT: external/foundationpose # <- Edit here SHA: # Empty defaults to latest REPO: ${{ steps.prep.outputs.repo }} run: | From 5d10952ac9dbabde3e7637cbbbbc5980fc4c5828 Mon Sep 17 00:00:00 2001 From: theo coulson Date: Wed, 15 Jul 2026 16:03:49 -0400 Subject: [PATCH 03/10] Copying the git workflows from futurhub --- .github/workflows/add-submodule.yml | 292 ++++++++++++++++++ .github/workflows/remove-submodule.yml | 216 +++++++++++++ .../update-submodule-on-dispatch.yml | 216 +++++++++++++ 3 files changed, 724 insertions(+) create mode 100644 .github/workflows/add-submodule.yml create mode 100644 .github/workflows/remove-submodule.yml create mode 100644 .github/workflows/update-submodule-on-dispatch.yml diff --git a/.github/workflows/add-submodule.yml b/.github/workflows/add-submodule.yml new file mode 100644 index 000000000..7e51641f9 --- /dev/null +++ b/.github/workflows/add-submodule.yml @@ -0,0 +1,292 @@ +name: Add submodule + +on: + workflow_dispatch: + inputs: + short_name: + description: "Submodule name, e.g. kr_ros2" + required: true + type: string + path: + description: "Path inside FuturHub, e.g. external/kr_ros2 or src/futur_code" + required: true + type: string + repo_url: + description: "Git URL, e.g. git@github.com:FuturHandRobotics/kr_ros2.git" + required: true + type: string + tracked_branch: + description: "Branch to track" + required: true + default: main + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + add-submodule: + runs-on: ubuntu-latest + + steps: + - name: Checkout FuturHub + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: false + token: ${{ secrets.FUTURHAND_SUBMODULE_UPDATE }} + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Configure GitHub auth for private submodules + run: | + git config --global url."https://x-access-token:${{ secrets.FUTURHAND_SUBMODULE_UPDATE }}@github.com/".insteadOf "https://github.com/" + + - name: Convert SSH GitHub URL to HTTPS for Actions + id: normalize + run: | + set -Eeuo pipefail + + url="${{ inputs.repo_url }}" + + if [[ "$url" == git@github.com:* ]]; then + repo_path="${url#git@github.com:}" + repo_path="${repo_path%.git}" + url="https://github.com/${repo_path}.git" + else + repo_path="${url#https://github.com/}" + repo_path="${repo_path%.git}" + fi + + echo "url=$url" >> "$GITHUB_OUTPUT" + echo "repo_path=$repo_path" >> "$GITHUB_OUTPUT" + + - name: Create child PR with notify-parent workflow + id: child_pr + env: + GH_TOKEN: ${{ secrets.FUTURHAND_SUBMODULE_UPDATE }} + CHILD_REPO: ${{ steps.normalize.outputs.repo_path }} + SHORT_NAME: ${{ inputs.short_name }} + PATH_IN_PARENT: ${{ inputs.path }} + TRACKED_BRANCH: ${{ inputs.tracked_branch }} + run: | + set -Eeuo pipefail + + workdir="$(mktemp -d)" + git clone "https://github.com/${CHILD_REPO}.git" "$workdir/child" + cd "$workdir/child" + + git fetch origin "$TRACKED_BRANCH" + git switch "$TRACKED_BRANCH" + + branch="automation/add-notify-parent-${SHORT_NAME}" + + if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + echo "Remote branch '$branch' already exists. Reusing it." + git fetch origin "$branch" + git switch -C "$branch" "origin/$branch" + else + echo "Remote branch '$branch' does not exist. Creating it from '$TRACKED_BRANCH'." + git switch -c "$branch" + fi + + mkdir -p .github/workflows + + SECRET_EXPR='$' + SECRET_EXPR="${SECRET_EXPR}{{ secrets.FUTURHAND_SUBMODULE_UPDATE }}" + + REPO_EXPR='$' + REPO_EXPR="${REPO_EXPR}{{ steps.prep.outputs.repo }}" + + cat > .github/workflows/notify-parent.yml <<'EOF' + name: Notify parent to bump submodule + + on: + push: + branches: [ __TRACKED_BRANCH__ ] # <- Edit here to tracked branch + workflow_dispatch: {} + + jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Prepare payload + id: prep + run: | + echo "sha=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT" + + - name: Dispatch to parent (FuturHub) # <- Edit here + env: + GH_PARENT_REPO: FuturHandRobotics/FuturHub # <- Edit here + GH_TOKEN: __SECRET_EXPR__ + SUBMODULE_PATH_IN_PARENT: __PATH_IN_PARENT__ # <- Edit here + SHA: # Empty defaults to latest + REPO: __REPO_EXPR__ + run: | + set -Eeuo pipefail + + # Normalize PAT: strip CR/LF so gh can use it in Authorization header + export GH_TOKEN="$(printf %s "$GH_TOKEN" | tr -d '\r\n')" + + payload=$(printf '{"event_type":"submodule_update","client_payload":{"submodule_path":"%s","sha":"%s","repo":"%s"}}' \ + "$SUBMODULE_PATH_IN_PARENT" "$SHA" "$REPO") + + echo "::group::Dispatch payload" + echo "$payload" + echo "::endgroup::" + + # Send request and capture body + status code (gh api → reliable JSON dispatch) + echo "$payload" > payload.json + resp_file="$(mktemp)" + gh api "repos/${GH_PARENT_REPO}/dispatches" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + --input payload.json \ + -X POST \ + -i > "$resp_file" + code="$(awk 'NR==1{print $2}' "$resp_file")" + + echo "::group::Response (HTTP $code)" + # Body may be empty on success (204); still show for transparency + if [ -s "$resp_file" ]; then cat "$resp_file"; else echo ""; fi + echo "::endgroup::" + + case "$code" in + 204) + echo "::notice title=Repository dispatch accepted::Parent=${GH_PARENT_REPO} path=${SUBMODULE_PATH_IN_PARENT} sha=${SHA}" + ;; + 401|403) + echo "::error title=Auth/Scope issue::HTTP $code. The PAT likely lacks access or is expired. + - Ensure the token is a **fine-grained PAT** granted to **${GH_PARENT_REPO}** + - Repo permissions: **Contents: Read and write** + - Secret name is correct in this repo + - Token not expired / revoked + See response above for details." + exit 1 + ;; + 404) + echo "::error title=Not found / access denied::HTTP 404. Check: + - GH_PARENT_REPO='${GH_PARENT_REPO}' is correct (owner/repo) + - PAT has access to that repo (same org/owner, correct repo selection) + - Repo is not private to a different owner without permission" + exit 1 + ;; + 422|400) + echo "::error title=Unprocessable payload::HTTP $code. Likely JSON shape, event_type, or required fields. + - event_type should match parent workflow: 'submodule_update' + - client_payload must include: submodule_path, sha, repo + - Validate quotes/escaping in payload (shown above)" + exit 1 + ;; + 5*) + echo "::warning title=GitHub server error::HTTP $code. Transient issue—consider retry/backoff." + + # Optional quick retry (1x): + sleep 2 + gh api "repos/${GH_PARENT_REPO}/dispatches" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + --input payload.json \ + -X POST \ + -i > "$resp_file" + code2="$(awk 'NR==1{print $2}' "$resp_file")" + + echo "::notice::Retry status: $code2" + if [ "$code2" != "204" ]; then + echo "::error::Retry failed. See response above." + exit 1 + fi + ;; + *) + echo "::error title=Unexpected status::HTTP $code. Inspect response above. Visit: https://docs.github.com/rest" + exit 1 + ;; + esac + EOF + + sed -i "s#__SECRET_EXPR__#${SECRET_EXPR}#g" .github/workflows/notify-parent.yml + sed -i "s#__TRACKED_BRANCH__#${TRACKED_BRANCH}#g" .github/workflows/notify-parent.yml + sed -i "s#__PATH_IN_PARENT__#${PATH_IN_PARENT}#g" .github/workflows/notify-parent.yml + sed -i "s#__REPO_EXPR__#${REPO_EXPR}#g" .github/workflows/notify-parent.yml + + git add .github/workflows/notify-parent.yml + + if git diff --cached --quiet; then + echo "No child workflow changes needed." + + existing_url="$(gh pr list \ + --repo "$CHILD_REPO" \ + --head "$branch" \ + --json url \ + --jq '.[0].url // empty')" + + echo "url=$existing_url" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git commit -m "ci: add notify parent workflow" + git push --set-upstream origin "$branch" + + pr_url="$(gh pr create \ + --repo "$CHILD_REPO" \ + --base "$TRACKED_BRANCH" \ + --head "$branch" \ + --title "[Automated PR] Add notify-parent workflow" \ + --body "Adds \`.github/workflows/notify-parent.yml\` so this repo can notify \`FuturHandRobotics/FuturHub\` when \`${TRACKED_BRANCH}\` moves. + + Parent submodule path: \`${PATH_IN_PARENT}\` + + ### Checklist + + - [ ] Workflow branch matches tracked branch: \`${TRACKED_BRANCH}\` + - [ ] \`SUBMODULE_PATH_IN_PARENT\` is correct: \`${PATH_IN_PARENT}\`")" + + echo "url=$pr_url" >> "$GITHUB_OUTPUT" + + - name: Add submodule + run: | + set -Eeuo pipefail + + short_name="${{ inputs.short_name }}" + path="${{ inputs.path }}" + tracked_branch="${{ inputs.tracked_branch }}" + repo_url="${{ steps.normalize.outputs.url }}" + + git submodule add -b "$tracked_branch" --name "$short_name" "$repo_url" "$path" + git config -f .gitmodules "submodule.${short_name}.branch" "$tracked_branch" + + git submodule sync -- "$path" + git submodule update --init "$path" + + git add .gitmodules "$path" + # Convert recorded HTTPS URLs in .gitmodules to SSH form for repository consumers + sed -i 's#https://github.com/#git@github.com:#g' .gitmodules + + - name: Create PR for new submodule + uses: peter-evans/create-pull-request@v6 + with: + token: "${{ secrets.FUTURHAND_SUBMODULE_UPDATE }}" + commit-message: "chore(submodule): add ${{ inputs.short_name }}" + branch: "add/${{ inputs.short_name }}" + delete-branch: true + title: "[Automated PR] Add submodule ${{ inputs.short_name }}" + body: | + Adds `${{ inputs.short_name }}` as a submodule at `${{ inputs.path }}`, tracking branch `${{ inputs.tracked_branch }}`. + + Child repo notify-parent PR: + - ${{ steps.child_pr.outputs.url }} + + ### Checklist + + - [ ] `.gitmodules` contains the correct path and branch (`cat .gitmodules`) + - [ ] `git submodule status` lists the new submodule + - [ ] README [Existing Submodules section](https://github.com/FuturHandRobotics/FuturHub/tree/add/${{ inputs.short_name }}#existing-submodules) has been updated + - [ ] Child repo notify-parent PR has been merged: ${{ steps.child_pr.outputs.url }} + labels: | + submodule + automations diff --git a/.github/workflows/remove-submodule.yml b/.github/workflows/remove-submodule.yml new file mode 100644 index 000000000..6da3003b8 --- /dev/null +++ b/.github/workflows/remove-submodule.yml @@ -0,0 +1,216 @@ +name: Remove submodule + +on: + workflow_dispatch: + inputs: + short_name: + description: "Submodule name, e.g. kr_ros2" + required: true + type: string + path: + description: "Path inside FuturHub, e.g. external/kr_ros2 or src/futur_code" + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + remove-submodule: + runs-on: ubuntu-latest + + steps: + - name: Checkout FuturHub + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: false + token: "${{ secrets.FUTURHAND_SUBMODULE_UPDATE }}" + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Configure GitHub auth for private submodules + run: | + git config --global url."https://x-access-token:${{ secrets.FUTURHAND_SUBMODULE_UPDATE }}@github.com/".insteadOf "https://github.com/" + + - name: Read submodule info + id: submodule_info + run: | + set -Eeuo pipefail + + path="${{ inputs.path }}" + + matching_key="" + while read -r key; do + value="$(git config -f .gitmodules --get "$key")" + if [[ "$value" == "$path" ]]; then + matching_key="$key" + break + fi + done < <(git config -f .gitmodules --name-only --get-regexp '^submodule\..*\.path$') + + if [[ -z "$matching_key" ]]; then + echo "::error::Submodule path '$path' not found in .gitmodules" + exit 1 + fi + + section="${matching_key%.path}" + + repo_url="$(git config -f .gitmodules --get "${section}.url")" + tracked_branch="$(git config -f .gitmodules --get "${section}.branch" || echo main)" + + if [[ "$repo_url" == git@github.com:* ]]; then + repo_path="${repo_url#git@github.com:}" + repo_path="${repo_path%.git}" + https_url="https://github.com/${repo_path}.git" + else + repo_path="${repo_url#https://github.com/}" + repo_path="${repo_path%.git}" + https_url="https://github.com/${repo_path}.git" + fi + + echo "section=$section" >> "$GITHUB_OUTPUT" + echo "repo_url=$repo_url" >> "$GITHUB_OUTPUT" + echo "https_url=$https_url" >> "$GITHUB_OUTPUT" + echo "repo_path=$repo_path" >> "$GITHUB_OUTPUT" + echo "tracked_branch=$tracked_branch" >> "$GITHUB_OUTPUT" + + - name: Create child PR removing notify-parent workflow + id: child_pr + env: + GH_TOKEN: ${{ secrets.FUTURHAND_SUBMODULE_UPDATE }} + CHILD_REPO: ${{ steps.submodule_info.outputs.repo_path }} + SHORT_NAME: ${{ inputs.short_name }} + PATH_IN_PARENT: ${{ inputs.path }} + TRACKED_BRANCH: ${{ steps.submodule_info.outputs.tracked_branch }} + run: | + set -Eeuo pipefail + + workdir="$(mktemp -d)" + git clone "https://github.com/${CHILD_REPO}.git" "$workdir/child" + cd "$workdir/child" + + git fetch origin "$TRACKED_BRANCH" + git switch "$TRACKED_BRANCH" + + branch="automation/remove-notify-parent-${SHORT_NAME}" + + if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + echo "Remote branch '$branch' already exists. Reusing it." + git fetch origin "$branch" + git switch -C "$branch" "origin/$branch" + else + echo "Remote branch '$branch' does not exist. Creating it from '$TRACKED_BRANCH'." + git switch -c "$branch" + fi + + if [[ ! -f ".github/workflows/notify-parent.yml" ]]; then + echo "No .github/workflows/notify-parent.yml found in child repo." + + existing_url="$(gh pr list \ + --repo "$CHILD_REPO" \ + --head "$branch" \ + --json url \ + --jq '.[0].url // empty')" + + if [[ -n "$existing_url" ]]; then + echo "url=$existing_url" >> "$GITHUB_OUTPUT" + else + echo "url=Not needed; child repo has no notify-parent workflow" >> "$GITHUB_OUTPUT" + fi + + exit 0 + fi + + git rm .github/workflows/notify-parent.yml + + if git diff --cached --quiet; then + echo "No child workflow changes needed." + + existing_url="$(gh pr list \ + --repo "$CHILD_REPO" \ + --head "$branch" \ + --json url \ + --jq '.[0].url // empty')" + + echo "url=$existing_url" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git commit -m "ci: remove notify parent workflow" + git push --set-upstream origin "$branch" + + existing_url="$(gh pr list \ + --repo "$CHILD_REPO" \ + --head "$branch" \ + --json url \ + --jq '.[0].url // empty')" + + if [[ -n "$existing_url" ]]; then + pr_url="$existing_url" + else + pr_url="$(gh pr create \ + --repo "$CHILD_REPO" \ + --base "$TRACKED_BRANCH" \ + --head "$branch" \ + --title "[Automated PR] Remove notify-parent workflow" \ + --body "Removes \`.github/workflows/notify-parent.yml\` because this repo is being removed as a submodule from \`FuturHandRobotics/FuturHub\`. + + Former parent submodule path: \`${PATH_IN_PARENT}\` + + ### Checklist + + - [ ] Confirm this repo should no longer notify \`FuturHandRobotics/FuturHub\` + - [ ] Confirm \`.github/workflows/notify-parent.yml\` has been removed")" + fi + + echo "url=$pr_url" >> "$GITHUB_OUTPUT" + + - name: Remove submodule + run: | + set -Eeuo pipefail + + short_name="${{ inputs.short_name }}" + path="${{ inputs.path }}" + + if ! git config -f .gitmodules --get-regexp '^submodule\..*\.path$' | awk '{print $2}' | grep -xq "$path"; then + echo "::error::Submodule path '$path' not found in .gitmodules" + exit 1 + fi + + git submodule deinit -f -- "$path" || true + git rm -f "$path" + rm -rf ".git/modules/$path" + + git submodule sync --recursive + + git add .gitmodules || true + + - name: Create PR for removed submodule + uses: peter-evans/create-pull-request@v6 + with: + token: "${{ secrets.FUTURHAND_SUBMODULE_UPDATE }}" + commit-message: "chore(submodule): remove ${{ inputs.short_name }}" + branch: "remove/${{ inputs.short_name }}" + delete-branch: true + title: "[Automated PR] Remove submodule ${{ inputs.short_name }}" + body: | + Removes `${{ inputs.short_name }}` from `${{ inputs.path }}`. + + Child repo notify-parent removal PR: + - ${{ steps.child_pr.outputs.url }} + + ### Checklist + + - [ ] Submodule path has been removed from `.gitmodules` (`cat .gitmodules`) + - [ ] Submodule gitlink has been removed from the repo (via `ls`) + - [ ] README [Existing Submodules section](https://github.com/FuturHandRobotics/FuturHub/tree/remove/${{ inputs.short_name }}#existing-submodules) has been updated + - [ ] Child repo notify-parent removal PR has been merged: ${{ steps.child_pr.outputs.url }} + + labels: | + submodule + automation \ No newline at end of file diff --git a/.github/workflows/update-submodule-on-dispatch.yml b/.github/workflows/update-submodule-on-dispatch.yml new file mode 100644 index 000000000..a3ef61e17 --- /dev/null +++ b/.github/workflows/update-submodule-on-dispatch.yml @@ -0,0 +1,216 @@ +name: Update submodule on dispatch + +on: + repository_dispatch: + types: [submodule_update] + workflow_dispatch: + inputs: + submodule_path: + description: Path of submodule in this repo (e.g., futur_docker, src/futur_code) # <- Edit here + required: true + type: string + sha: + description: Pin to this commit (optional) + required: false + type: string + repo: + description: Source repo sending the update + required: true + type: string + +concurrency: + group: submodule-bump-${{ github.event.client_payload.submodule_path || inputs.submodule_path }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + bump-submodule: + runs-on: ubuntu-latest + # Pull values from repository_dispatch payload OR workflow_dispatch inputs + env: + SUBMODULE_PATH: ${{ github.event.client_payload.submodule_path || inputs.submodule_path }} + PIN_SHA: ${{ github.event.client_payload.sha || inputs.sha }} + SRC_REPO: ${{ github.event.client_payload.repo || inputs.repo }} + steps: + - name: Show incoming values + run: | + echo "SUBMODULE_PATH=${SUBMODULE_PATH}" + echo "PIN_SHA=${PIN_SHA:-}" + echo "SRC_REPO=${SRC_REPO}" + + - name: Checkout parent repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: false + + - name: Validate payload and submodule presence + id: validate + shell: bash + run: | + set -euo pipefail + test -n "${SUBMODULE_PATH:-}" || { echo "::error::Missing client_payload.submodule_path"; exit 1; } + if ! git config -f .gitmodules --get-regexp '^submodule\..*\.path$' | awk '{print $2}' | grep -xq "$SUBMODULE_PATH"; then + echo "::error::Submodule path '$SUBMODULE_PATH' not found in .gitmodules" + exit 1 + fi + # Old commit (what's currently recorded for the submodule at HEAD) + OLD_SHA="$(git ls-tree HEAD -- "$SUBMODULE_PATH" | awk '{print $3}')" + echo "old_sha=$OLD_SHA" >> "$GITHUB_OUTPUT" + + - name: Read submodule metadata (name + tracked branch) + id: submeta + shell: bash + run: | + set -euo pipefail + NAME="$(git config -f .gitmodules --get-regexp '^submodule\..*\.path$' \ + | awk -v p="$SUBMODULE_PATH" '$2==p{match($1,/^submodule\.([^.]*)\.path$/,m); print m[1]}')" + BRANCH="$(git config -f .gitmodules --get "submodule.${NAME}.branch" || true)" + if [ -z "${BRANCH:-}" ]; then BRANCH=main; fi + echo "name=$NAME" >> "$GITHUB_OUTPUT" + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" + + - name: Sync submodule metadata (url/branch) once + run: git submodule sync -- "$SUBMODULE_PATH" + + - name: Authenticate for submodules (force HTTPS for target) + env: + SUBMODULES_PAT: ${{ secrets.FUTURHAND_SUBMODULE_UPDATE }} # PAT with Contents:read on child repo(s) + SUBMODULE_PATH: ${{ env.SUBMODULE_PATH }} + run: | + set -euo pipefail + if [ -z "${SUBMODULES_PAT:-}" ]; then + echo "::error::FUTURHAND_SUBMODULE_UPDATE is empty. Add the PAT secret in the parent repo." + exit 1 + fi + + # Optional global rewrites as a safety net + git config --global url."https://x-access-token:${SUBMODULES_PAT}@github.com/".insteadOf ssh://git@github.com/ + git config --global url."https://x-access-token:${SUBMODULES_PAT}@github.com/".insteadOf git@github.com: + git config --global url."https://x-access-token:${SUBMODULES_PAT}@github.com/".insteadOf https://github.com/ + + # Resolve submodule for the given path + name="$(git config -f .gitmodules --get-regexp '^submodule\..*\.path$' \ + | awk -v p="$SUBMODULE_PATH" '$2==p{match($1,/^submodule\.([^.]*)\.path$/,m); print m[1]}')" + if [ -z "${name:-}" ]; then + echo "::error::Could not resolve submodule name for path '${SUBMODULE_PATH}'" + exit 1 + fi + + # Build tokenized HTTPS URL and override in local config ONLY (do not sync afterwards) + orig_url="$(git config -f .gitmodules --get "submodule.${name}.url")" + case "$orig_url" in + git@github.com:*) repo="${orig_url#git@github.com:}";; + https://github.com/*) repo="${orig_url#https://github.com/}";; + *) repo="$orig_url";; + esac + https="https://x-access-token:${SUBMODULES_PAT}@github.com/${repo}" + git config submodule."$name".url "$https" + + echo "::notice::Effective submodule URL now: $(git config --get submodule."$name".url)" + + - name: Initialize submodule (robust) + run: | + set -euo pipefail + # This will use the overridden HTTPS+PAT URL from .git/config + if ! git submodule update --init "$SUBMODULE_PATH"; then + echo "::warning::Init failed; cleaning path and retrying" + rm -rf -- "$SUBMODULE_PATH" + git submodule update --init "$SUBMODULE_PATH" + fi + + - name: Update submodule to desired commit or tracked branch + id: update + shell: bash + run: | + set -euo pipefail + # Ensure we have full history in the submodule (no shallow surprises) + git -C "$SUBMODULE_PATH" fetch --tags origin "+refs/heads/*:refs/remotes/origin/*" || true + + if [ -n "${PIN_SHA:-}" ]; then + # Pin to specific commit from payload, if it exists remotely + if git -C "$SUBMODULE_PATH" cat-file -e "${PIN_SHA}^{commit}" 2>/dev/null; then + git -C "$SUBMODULE_PATH" checkout --detach "$PIN_SHA" + else + echo "::warning::PIN_SHA ${PIN_SHA} not found after fetch; falling back to tracked branch" + git submodule update --remote "$SUBMODULE_PATH" + fi + else + # Follow tracked branch from .gitmodules + git submodule update --remote "$SUBMODULE_PATH" + fi + + # Final sanity: ensure the submodule worktree is detached at a commit + NEW_SHA="$(git -C "$SUBMODULE_PATH" rev-parse HEAD)" + echo "new_sha=$NEW_SHA" >> "$GITHUB_OUTPUT" + + # Stage the submodule change in the parent + git add "$SUBMODULE_PATH" + + if git diff --cached --quiet -- "$SUBMODULE_PATH"; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "::notice::No change detected for $SUBMODULE_PATH (already at $NEW_SHA)" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "::notice::Detected update for $SUBMODULE_PATH -> $NEW_SHA" + fi + + - name: Create PR for submodule bump + if: steps.update.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.FUTURHAND_SUBMODULE_UPDATE }} + commit-message: | + chore(submodule): bump ${{ env.SUBMODULE_PATH }} to ${{ steps.update.outputs.new_sha }} + branch: bump/${{ env.SUBMODULE_PATH }} + delete-branch: true + title: | + [Automated PR] Bump submodule ${{ env.SUBMODULE_PATH }} to ${{ steps.update.outputs.new_sha }} + body: | + ### Summary + + Bump submodule `${{ env.SUBMODULE_PATH }}` to commit `${{ steps.update.outputs.new_sha }}`. + + Trigger source: `${{ env.SRC_REPO }}` payload${{ env.PIN_SHA && format(' pinned to `{0}`', env.PIN_SHA) || ' (tracked branch head)' }}. + + Previous submodule commit recorded in parent: `${{ steps.validate.outputs.old_sha }}`. + + ### Related Issues + + N/A + + ### Pull requests that need to be reviewed before this one + N/A + + ### Changes + - Update gitlink for `${{ env.SUBMODULE_PATH }}` to `${{ steps.update.outputs.new_sha }}` + + ### Testing + + - Dispatch received and validated (`repository_dispatch` → `submodule_update`). + - Submodule initialized/synced and updated (pin or tracked branch). + - Parent tree reflects new gitlink; CI should verify build against updated submodule. + + ### Reviewer checklist + - [ ] **Diff matches the PR title** (path `${{ env.SUBMODULE_PATH }}` → `${{ steps.update.outputs.new_sha }}`) and **makes only that change** + _Tip: Files changed should be 1; “1 insertion, 1 deletion” on the submodule entry._ + + + - [ ] **Commit exists on the child repo’s tracked branch** and is the latest commit + Child branch: [`${{ env.SRC_REPO }}@${{ steps.submeta.outputs.branch }}`](https://github.com/${{ env.SRC_REPO }}/tree/${{ steps.submeta.outputs.branch }}) + Commits feed: https://github.com/${{ env.SRC_REPO }}/commits/${{ steps.submeta.outputs.branch }} + This commit: https://github.com/${{ env.SRC_REPO }}/commit/${{ steps.update.outputs.new_sha }} + + + - [ ] Approve and merge! It's best we don't keep these automated PRs lingering for too long! + + labels: | + submodule + automation + + - name: Nothing to do (already up to date) + if: steps.update.outputs.changed != 'true' + run: echo "Submodule up to date; no PR opened." From 655c10cfa008e05a7a71fec61e6d70b3e7311775 Mon Sep 17 00:00:00 2001 From: theo coulson Date: Wed, 15 Jul 2026 16:05:35 -0400 Subject: [PATCH 04/10] start of grpc dockerfile New dockerfile inhereting from a corrected image that has been published. --- docker/dockerfile.grpc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docker/dockerfile.grpc diff --git a/docker/dockerfile.grpc b/docker/dockerfile.grpc new file mode 100644 index 000000000..af1250236 --- /dev/null +++ b/docker/dockerfile.grpc @@ -0,0 +1,3 @@ +FROM shingarey/foundationpose_custom_cuda121:latest + +RUN pip install grpcio grpcio-tools --break-system-packages \ No newline at end of file From 171ffe1750c9a90482138eb76635d8ca3c17341e Mon Sep 17 00:00:00 2001 From: theo coulson Date: Fri, 17 Jul 2026 10:03:44 -0400 Subject: [PATCH 05/10] Dockerfile v1 ready to work --- bundlesdf/mycuda/setup.py | 4 ++-- docker/dockerfile.grpc | 14 +++++++++++++- docker/run_container.sh | 16 ++++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/bundlesdf/mycuda/setup.py b/bundlesdf/mycuda/setup.py index ad0033494..55fa5967a 100644 --- a/bundlesdf/mycuda/setup.py +++ b/bundlesdf/mycuda/setup.py @@ -15,8 +15,8 @@ code_dir = os.path.dirname(os.path.realpath(__file__)) -nvcc_flags = ['-Xcompiler', '-O3', '-std=c++14', '-U__CUDA_NO_HALF_OPERATORS__', '-U__CUDA_NO_HALF_CONVERSIONS__', '-U__CUDA_NO_HALF2_OPERATORS__'] -c_flags = ['-O3', '-std=c++14'] +nvcc_flags = ['-Xcompiler', '-O3', '-std=c++17', '-U__CUDA_NO_HALF_OPERATORS__', '-U__CUDA_NO_HALF_CONVERSIONS__', '-U__CUDA_NO_HALF2_OPERATORS__'] +c_flags = ['-O3', '-std=c++17'] setup( name='common', diff --git a/docker/dockerfile.grpc b/docker/dockerfile.grpc index af1250236..2004d533e 100644 --- a/docker/dockerfile.grpc +++ b/docker/dockerfile.grpc @@ -1,3 +1,15 @@ +# Intermediate build image which compiles the gRPC commands. +# Goes at the top since Docker uses the final FROM as the resulting image +FROM python:3.11-slim AS protogen +RUN pip install --no-cache-dir grpcio-tools==1.68.0 +RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client +RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /root/.ssh/known_hosts +RUN --mount=type=ssh git clone --depth 1 --branch v1.0.1 \ + git@github.com:FuturHandRobotics/futur_grpc.git /proto-src +WORKDIR /proto-src +RUN chmod +x scripts/generate.sh && ./scripts/generate.sh + FROM shingarey/foundationpose_custom_cuda121:latest -RUN pip install grpcio grpcio-tools --break-system-packages \ No newline at end of file +COPY --from=protogen /proto-src /tmp/proto-src +RUN pip install --no-cache-dir --break-system-packages /tmp/proto-src && rm -rf /tmp/proto-src \ No newline at end of file diff --git a/docker/run_container.sh b/docker/run_container.sh index 85035714e..aa5cc8c07 100644 --- a/docker/run_container.sh +++ b/docker/run_container.sh @@ -1,3 +1,15 @@ +set -euo pipefail + +DOCKER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +DIR=$(cd "$DOCKER_DIR/.." && pwd) +IMAGE_NAME="foundationpose:grpc" + +if [ -z "${SSH_AUTH_SOCK:-}" ]; then + echo "SSH_AUTH_SOCK is not set - start an ssh-agent and ssh-add a key with access to FuturHandRobotics/futur_grpc first." >&2 + exit 1 +fi + +DOCKER_BUILDKIT=1 docker build --network host --ssh default -f "$DOCKER_DIR/dockerfile.grpc" -t "$IMAGE_NAME" "$DOCKER_DIR" + docker rm -f foundationpose -DIR=$(pwd)/../ -xhost + && docker run --gpus all --env NVIDIA_DISABLE_REQUIRE=1 -it --network=host --name foundationpose --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v $DIR:$DIR -v /home:/home -v /mnt:/mnt -v /tmp/.X11-unix:/tmp/.X11-unix -v /tmp:/tmp --ipc=host -e DISPLAY=${DISPLAY} -e GIT_INDEX_FILE foundationpose:latest bash -c "cd $DIR && bash" +xhost + && docker run --gpus all --env NVIDIA_DISABLE_REQUIRE=1 -it --network=host --name foundationpose --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v $DIR:$DIR -v /home:/home -v /mnt:/mnt -v /tmp/.X11-unix:/tmp/.X11-unix -v /tmp:/tmp --ipc=host -e DISPLAY=${DISPLAY} -e GIT_INDEX_FILE $IMAGE_NAME bash -c "cd $DIR && bash" From a8220b9f9eeb8d4128dae52f71336441722b4b85 Mon Sep 17 00:00:00 2001 From: Theo Date: Mon, 20 Jul 2026 11:32:44 -0400 Subject: [PATCH 06/10] grpc working, now to do it with FP --- docker/dockerfile.cuda128 | 141 ++++++++++++++++++++++++++++++++++++++ docker/dockerfile.grpc | 8 ++- docker/run_container.sh | 2 +- test_server.py | 16 +++++ 4 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 docker/dockerfile.cuda128 create mode 100644 test_server.py diff --git a/docker/dockerfile.cuda128 b/docker/dockerfile.cuda128 new file mode 100644 index 000000000..045dd5100 --- /dev/null +++ b/docker/dockerfile.cuda128 @@ -0,0 +1,141 @@ +# ============================================================================= +# Intermediate build image which compiles the gRPC commands. +# Goes at the top since Docker uses the final FROM as the resulting image +# (unchanged from your original dockerfile.grpc) +# ============================================================================= +FROM python:3.11-slim AS protogen +# Pin protobuf here to match futur_grpc's pyproject.toml runtime pin (protobuf==5.28.0). +# Otherwise pip resolves grpcio-tools' loose protobuf constraint independently (currently 5.28.1), +# and generate.sh stamps the generated _pb2.py gencode with that newer version, which is then +# incompatible with the older protobuf runtime that futur_grpc actually installs downstream. +RUN pip install --no-cache-dir grpcio-tools==1.68.0 protobuf==5.28.0 +RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client +RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /root/.ssh/known_hosts +RUN --mount=type=ssh git clone --depth 1 --branch v1.0.2 \ + git@github.com:FuturHandRobotics/futur_grpc.git /proto-src +WORKDIR /proto-src +RUN chmod +x scripts/generate.sh && ./scripts/generate.sh + + +# ============================================================================= +# FoundationPose base image, CUDA 12.8 / RTX 50-series (sm_120) capable. +# +# There is currently no published Docker image for this (checked NVlabs/ +# FoundationPose issues/PRs on 2026-07-17 - see PR #369, which explicitly +# says "There are currently no Docker images that can run on RTX 50 series +# GPU"). This stage rebuilds the environment from scratch, adapted from the +# official docker/dockerfile (which targets cudagl:11.3.0 + torch 2.0/cu118) +# with the version bumps and fixes discussed in that PR: +# - PyTorch 2.7.1+cu128 (RTX 50 / sm_120 requires PyTorch 2.7+ / CUDA 12.8+) +# - PyTorch3D built from source (no prebuilt wheels exist yet for sm_120) +# - TORCH_CUDA_ARCH_LIST includes 12.0 for Blackwell, plus older archs so +# the same image still works on your existing 30/40-series boxes +# - conda env on Python 3.9, per the RTX 50 setup notes in the FP readme +# +# NOTE - this image does NOT bake in the FoundationPose repo itself (same as +# the official dockerfile - it expects you to bind-mount the repo and run +# build_all.sh / build_all_conda.sh inside the container). Because PR #369 +# is still open/unmerged as of this writing, you'll need to apply its fixes +# to whatever checkout you mount in, OR clone wualbert's branch instead: +# git fetch https://github.com/wualbert/FoundationPose.git && git checkout FETCH_HEAD +# The fixes you need on top of NVlabs/FoundationPose@main are: +# - bundlesdf/mycuda/common.cu: .type() -> .scalar_type() (3 call sites) +# - bundlesdf/mycuda/setup.py: C++14 -> C++17 +# - mycpp/src/app/pybind_api.cpp: printf %d -> %zu for size_t values +# - Utils.py: `from mycpp.build.mycpp import ...` -> `from mycpp import ...` +# ============================================================================= +FROM nvidia/cuda:12.8.0-devel-ubuntu22.04 +# ^ If this tag 404s by the time you build, swap in whatever 12.8.x devel +# tag is current on hub.docker.com/r/nvidia/cuda/tags - the toolkit patch +# version doesn't matter here, just needs to be a 12.8+ *devel* image. + +ENV TZ=US/Pacific +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update --fix-missing && \ + apt-get install -y libgtk2.0-dev && \ + apt-get install -y wget bzip2 ca-certificates curl git vim tmux g++ gcc \ + build-essential cmake checkinstall gfortran libjpeg8-dev libtiff-dev \ + pkg-config yasm libavcodec-dev libavformat-dev libswscale-dev \ + libdc1394-dev libxine2-dev libv4l-dev qtbase5-dev libgtk2.0-dev libtbb-dev \ + libatlas-base-dev libfaac-dev libmp3lame-dev libtheora-dev libvorbis-dev \ + libxvidcore-dev libopencore-amrnb-dev libopencore-amrwb-dev x264 \ + v4l-utils libprotobuf-dev protobuf-compiler libgoogle-glog-dev \ + libgflags-dev libgphoto2-dev libhdf5-dev doxygen libflann-dev \ + libboost-all-dev proj-data libproj-dev libyaml-cpp-dev cmake-curses-gui \ + libzmq3-dev freeglut3-dev + +RUN cd / && git clone https://github.com/pybind/pybind11 &&\ + cd pybind11 && git checkout v2.10.0 &&\ + mkdir build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Release -DPYBIND11_INSTALL=ON -DPYBIND11_TEST=OFF &&\ + make -j"$(nproc)" && make install + +RUN cd / && wget https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz &&\ + tar xvzf ./eigen-3.4.0.tar.gz &&\ + cd eigen-3.4.0 &&\ + mkdir build &&\ + cd build &&\ + cmake .. &&\ + make install + +SHELL ["/bin/bash", "--login", "-c"] + +RUN cd / && wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /miniconda.sh && \ + /bin/bash /miniconda.sh -b -p /opt/conda &&\ + ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh &&\ + echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc &&\ + /bin/bash -c "source ~/.bashrc" && \ + /opt/conda/bin/conda update -n base -c defaults conda -y &&\ + /opt/conda/bin/conda create -n my python=3.9 + +ENV PATH $PATH:/opt/conda/envs/my/bin + +# RTX 50-series = sm_120; keep older archs too so this image still runs on +# your 30/40-series machines without a rebuild. Trim this list if you want +# faster builds and only ever target one GPU generation. +ENV TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9;9.0;12.0" +ENV CUDA_HOME=/usr/local/cuda +ENV PATH=$CUDA_HOME/bin:$PATH +ENV LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH +# PyTorch3D's build script is picky about the host compiler inside conda - +# point it at the system g++ rather than conda's, per the working setup +# reported in FoundationPose issue #398. +ENV CC=/usr/bin/gcc +ENV CXX=/usr/bin/g++ +ENV CUDAHOSTCXX=/usr/bin/g++ + +RUN conda init bash &&\ + echo "conda activate my" >> ~/.bashrc &&\ + conda activate my &&\ + pip install torch==2.7.1 torchvision==0.22.1 torchaudio==2.7.1 --index-url https://download.pytorch.org/whl/cu128 &&\ + pip install -U pip setuptools wheel cmake ninja &&\ + pip install --no-build-isolation "git+https://github.com/facebookresearch/pytorch3d.git@stable" &&\ + pip install scipy joblib scikit-learn ruamel.yaml trimesh pyyaml opencv-python imageio open3d transformations warp-lang einops kornia pyrender + +# Kaolin is only needed for the model-free setup. Pin/branch may need to +# change to whatever tag actually supports torch 2.7/cu128 by the time you +# build - check https://kaolin.readthedocs.io/en/latest/notes/installation.html. +# Comment this block out if you don't need the model-free path. +RUN cd / && git clone --recursive https://github.com/NVIDIAGameWorks/kaolin +RUN conda activate my && cd /kaolin &&\ + FORCE_CUDA=1 python setup.py develop + +RUN cd / && git clone https://github.com/NVlabs/nvdiffrast &&\ + conda activate my && cd /nvdiffrast && pip install --no-build-isolation . + +ENV OPENCV_IO_ENABLE_OPENEXR=1 + +RUN conda activate my &&\ + pip install scikit-image meshcat webdataset omegaconf pypng roma seaborn opencv-contrib-python openpyxl wandb imgaug Ninja xlsxwriter timm albumentations xatlas rtree nodejs jupyterlab objaverse g4f ultralytics==8.0.120 pycocotools videoio numba &&\ + conda install -y -c anaconda h5py + +ENV SHELL=/bin/bash +RUN ln -sf /bin/bash /bin/sh + + +# ============================================================================= +# Final image: add your gRPC package on top of the CUDA 12.8 FoundationPose base +# ============================================================================= +COPY --from=protogen /proto-src /tmp/proto-src +RUN conda activate my && pip install --no-cache-dir /tmp/proto-src && rm -rf /tmp/proto-src \ No newline at end of file diff --git a/docker/dockerfile.grpc b/docker/dockerfile.grpc index 2004d533e..328781084 100644 --- a/docker/dockerfile.grpc +++ b/docker/dockerfile.grpc @@ -1,10 +1,14 @@ # Intermediate build image which compiles the gRPC commands. # Goes at the top since Docker uses the final FROM as the resulting image FROM python:3.11-slim AS protogen -RUN pip install --no-cache-dir grpcio-tools==1.68.0 +# Pin protobuf here to match futur_grpc's pyproject.toml runtime pin (protobuf==5.28.0). +# Otherwise pip resolves grpcio-tools' loose protobuf constraint independently (currently 5.28.1), +# and generate.sh stamps the generated _pb2.py gencode with that newer version, which is then +# incompatible with the older protobuf runtime that futur_grpc actually installs downstream. +RUN pip install --no-cache-dir grpcio-tools==1.68.0 protobuf==5.28.0 RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /root/.ssh/known_hosts -RUN --mount=type=ssh git clone --depth 1 --branch v1.0.1 \ +RUN --mount=type=ssh git clone --depth 1 --branch v1.0.2 \ git@github.com:FuturHandRobotics/futur_grpc.git /proto-src WORKDIR /proto-src RUN chmod +x scripts/generate.sh && ./scripts/generate.sh diff --git a/docker/run_container.sh b/docker/run_container.sh index aa5cc8c07..70de8d069 100644 --- a/docker/run_container.sh +++ b/docker/run_container.sh @@ -9,7 +9,7 @@ if [ -z "${SSH_AUTH_SOCK:-}" ]; then exit 1 fi -DOCKER_BUILDKIT=1 docker build --network host --ssh default -f "$DOCKER_DIR/dockerfile.grpc" -t "$IMAGE_NAME" "$DOCKER_DIR" +DOCKER_BUILDKIT=1 docker build --network host --ssh default -f "$DOCKER_DIR/dockerfile.cuda128" -t "$IMAGE_NAME" "$DOCKER_DIR" docker rm -f foundationpose xhost + && docker run --gpus all --env NVIDIA_DISABLE_REQUIRE=1 -it --network=host --name foundationpose --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v $DIR:$DIR -v /home:/home -v /mnt:/mnt -v /tmp/.X11-unix:/tmp/.X11-unix -v /tmp:/tmp --ipc=host -e DISPLAY=${DISPLAY} -e GIT_INDEX_FILE $IMAGE_NAME bash -c "cd $DIR && bash" diff --git a/test_server.py b/test_server.py new file mode 100644 index 000000000..ebfc8aa8e --- /dev/null +++ b/test_server.py @@ -0,0 +1,16 @@ +import grpc +from concurrent import futures +from futur_grpc import PoseResponse, PoseEstimateServicer, add_PoseEstimateServicer_to_server + +class EchoServicer(PoseEstimateServicer): + def TrackStream(self, request_iterator, context): + for i, request in enumerate(request_iterator): + print(f"[server] got frame {i}, width={request.width}, height={request.height}") + yield PoseResponse(translation=[0.0, 0.0, float(i)], rotation=[0, 0, 0, 1], success=True) + +server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) +add_PoseEstimateServicer_to_server(EchoServicer(), server) +server.add_insecure_port(' [::]:50051') +server.start() +print("[server] listening on 50051") +server.wait_for_termination() \ No newline at end of file From 896fd0b8708e0bad0d109f11e69608e29e529df2 Mon Sep 17 00:00:00 2001 From: Theo Coulson Date: Mon, 20 Jul 2026 14:53:55 -0400 Subject: [PATCH 07/10] first not-quite working server with, like. more versioning problems? --- .gitignore | 1 + docker/dockerfile.cuda128 | 14 +++--- docker/dockerfile.grpc | 4 +- docker/run_container.sh | 2 +- grpc_server.py | 97 +++++++++++++++++++++++++++++++++++++++ test_server.py | 14 ++++-- 6 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 grpc_server.py diff --git a/.gitignore b/.gitignore index cc59bdd93..563d22517 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ dist/ # *.jpg debug/ weights/* +meshes/ downloads/ eggs/ .eggs/ diff --git a/docker/dockerfile.cuda128 b/docker/dockerfile.cuda128 index 045dd5100..59939842c 100644 --- a/docker/dockerfile.cuda128 +++ b/docker/dockerfile.cuda128 @@ -4,14 +4,16 @@ # (unchanged from your original dockerfile.grpc) # ============================================================================= FROM python:3.11-slim AS protogen -# Pin protobuf here to match futur_grpc's pyproject.toml runtime pin (protobuf==5.28.0). -# Otherwise pip resolves grpcio-tools' loose protobuf constraint independently (currently 5.28.1), -# and generate.sh stamps the generated _pb2.py gencode with that newer version, which is then -# incompatible with the older protobuf runtime that futur_grpc actually installs downstream. -RUN pip install --no-cache-dir grpcio-tools==1.68.0 protobuf==5.28.0 +# grpcio-tools==1.68.0's bundled protoc always stamps generated _pb2.py gencode as +# protobuf 5.28.1 - that's baked into the wheel itself and does NOT depend on which +# protobuf package version gets installed alongside it here. What actually matters is +# the runtime protobuf version installed where the generated code executes (pinned in +# futur_grpc's pyproject.toml), which must be >= 5.28.1 per protobuf's version guarantee. +# Pinned here only for consistency with that runtime pin, not because it affects gencode. +RUN pip install --no-cache-dir grpcio-tools==1.68.0 RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /root/.ssh/known_hosts -RUN --mount=type=ssh git clone --depth 1 --branch v1.0.2 \ +RUN --mount=type=ssh git clone --depth 1 --branch v1.1.2 \ git@github.com:FuturHandRobotics/futur_grpc.git /proto-src WORKDIR /proto-src RUN chmod +x scripts/generate.sh && ./scripts/generate.sh diff --git a/docker/dockerfile.grpc b/docker/dockerfile.grpc index 328781084..353b615c7 100644 --- a/docker/dockerfile.grpc +++ b/docker/dockerfile.grpc @@ -5,10 +5,10 @@ FROM python:3.11-slim AS protogen # Otherwise pip resolves grpcio-tools' loose protobuf constraint independently (currently 5.28.1), # and generate.sh stamps the generated _pb2.py gencode with that newer version, which is then # incompatible with the older protobuf runtime that futur_grpc actually installs downstream. -RUN pip install --no-cache-dir grpcio-tools==1.68.0 protobuf==5.28.0 +RUN pip install --no-cache-dir grpcio-tools==1.68.0 RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /root/.ssh/known_hosts -RUN --mount=type=ssh git clone --depth 1 --branch v1.0.2 \ +RUN --mount=type=ssh git clone --depth 1 --branch v1.1.2 \ git@github.com:FuturHandRobotics/futur_grpc.git /proto-src WORKDIR /proto-src RUN chmod +x scripts/generate.sh && ./scripts/generate.sh diff --git a/docker/run_container.sh b/docker/run_container.sh index 70de8d069..aa5cc8c07 100644 --- a/docker/run_container.sh +++ b/docker/run_container.sh @@ -9,7 +9,7 @@ if [ -z "${SSH_AUTH_SOCK:-}" ]; then exit 1 fi -DOCKER_BUILDKIT=1 docker build --network host --ssh default -f "$DOCKER_DIR/dockerfile.cuda128" -t "$IMAGE_NAME" "$DOCKER_DIR" +DOCKER_BUILDKIT=1 docker build --network host --ssh default -f "$DOCKER_DIR/dockerfile.grpc" -t "$IMAGE_NAME" "$DOCKER_DIR" docker rm -f foundationpose xhost + && docker run --gpus all --env NVIDIA_DISABLE_REQUIRE=1 -it --network=host --name foundationpose --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v $DIR:$DIR -v /home:/home -v /mnt:/mnt -v /tmp/.X11-unix:/tmp/.X11-unix -v /tmp:/tmp --ipc=host -e DISPLAY=${DISPLAY} -e GIT_INDEX_FILE $IMAGE_NAME bash -c "cd $DIR && bash" diff --git a/grpc_server.py b/grpc_server.py new file mode 100644 index 000000000..65f0044fb --- /dev/null +++ b/grpc_server.py @@ -0,0 +1,97 @@ +from estimater import * +import argparse +from concurrent import futures + +import grpc +from futur_grpc import ( + Pose, + Empty, + PoseEstimateServicer, + add_PoseEstimateServicer_to_server, +) + + +class PoseEstimateService(PoseEstimateServicer): + def __init__(self, est, est_refine_iter, track_refine_iter): + self.est = est + self.est_refine_iter = est_refine_iter + self.track_refine_iter = track_refine_iter + + def _unpack_frame(self, request): + rgb = np.frombuffer(request.rgb_data, dtype=np.uint8).reshape(request.height, request.width, 3) + depth = np.frombuffer(request.depth_data, dtype=np.float32).reshape(request.height, request.width) + K = np.array(request.intrinsics, dtype=np.float32).reshape(3, 3) + return rgb, depth, K + + def TrackStream(self, request_iterator, context): + for request in request_iterator: + rgb, depth, K = self._unpack_frame(request) + + if self.est.pose_last is None: + if not request.HasField('mask'): + logging.warning('no pose to track from and no mask provided to register; skipping frame') + yield Pose(success=False) + continue + mask = np.frombuffer(request.mask, dtype=np.uint8).reshape(request.height, request.width).astype(bool) + pose = self.est.register(K=K, rgb=rgb, depth=depth, ob_mask=mask, iteration=self.est_refine_iter) + else: + pose = self.est.track_one(rgb=rgb, depth=depth, K=K, iteration=self.track_refine_iter) + + yield Pose( + translation=pose[:3, 3].tolist(), + rotation=pose[:3, :3].reshape(-1).tolist(), + success=True, + ) + + def Reset(self, request, context): + self.est.pose_last = None + return Empty() + + def SeedPose(self, request, context): + pose = np.eye(4, dtype=np.float32) + pose[:3, :3] = np.array(request.rotation, dtype=np.float32).reshape(3, 3) + pose[:3, 3] = np.array(request.translation, dtype=np.float32) + self.est.pose_last = torch.as_tensor(pose, device='cuda', dtype=torch.float) + return Empty() + + +if __name__ == '__main__': + code_dir = os.path.dirname(os.path.realpath(__file__)) + parser = argparse.ArgumentParser() + parser.add_argument('mesh_file', type=str, help='path to the object mesh to load (e.g. textured_simple.obj)',\ + default='{code_dir}/demo_data/mustard0/mesh/textured_simple.obj') + parser.add_argument('--port', type=int, default=50051) + parser.add_argument('--est_refine_iter', type=int, default=5) + parser.add_argument('--track_refine_iter', type=int, default=2) + parser.add_argument('--debug', type=int, default=0) + parser.add_argument('--debug_dir', type=str, default=f'{os.path.dirname(os.path.realpath(__file__))}/debug') + args = parser.parse_args() + + set_logging_format() + set_seed(0) + + mesh = trimesh.load(args.mesh_file) + + scorer = ScorePredictor() + refiner = PoseRefinePredictor() + glctx = dr.RasterizeCudaContext() + est = FoundationPose( + model_pts=mesh.vertices, + model_normals=mesh.vertex_normals, + mesh=mesh, + scorer=scorer, + refiner=refiner, + debug_dir=args.debug_dir, + debug=args.debug, + glctx=glctx, + ) + logging.info('estimator initialization done') + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + add_PoseEstimateServicer_to_server( + PoseEstimateService(est, args.est_refine_iter, args.track_refine_iter), server + ) + server.add_insecure_port(f'[::]:{args.port}') + server.start() + logging.info(f'listening on {args.port}') + server.wait_for_termination() diff --git a/test_server.py b/test_server.py index ebfc8aa8e..acc2af71c 100644 --- a/test_server.py +++ b/test_server.py @@ -1,16 +1,24 @@ import grpc from concurrent import futures -from futur_grpc import PoseResponse, PoseEstimateServicer, add_PoseEstimateServicer_to_server +from futur_grpc import Pose, Empty, PoseEstimateServicer, add_PoseEstimateServicer_to_server class EchoServicer(PoseEstimateServicer): def TrackStream(self, request_iterator, context): for i, request in enumerate(request_iterator): print(f"[server] got frame {i}, width={request.width}, height={request.height}") - yield PoseResponse(translation=[0.0, 0.0, float(i)], rotation=[0, 0, 0, 1], success=True) + yield Pose(translation=[0.0, 0.0, float(i)], rotation=[1, 0, 0, 0, 1, 0, 0, 0, 1], success=True) + + def Reset(self, request, context): + print("[server] reset") + return Empty() + + def SeedPose(self, request, context): + print(f"[server] seed pose translation={list(request.translation)}") + return Empty() server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) add_PoseEstimateServicer_to_server(EchoServicer(), server) -server.add_insecure_port(' [::]:50051') +server.add_insecure_port('[::]:50051') server.start() print("[server] listening on 50051") server.wait_for_termination() \ No newline at end of file From 3508c41212d2f98a8787aea31f6737e3b24933eb Mon Sep 17 00:00:00 2001 From: Theo Coulson Date: Mon, 20 Jul 2026 16:41:30 -0400 Subject: [PATCH 08/10] Versioning + working server --- docker/dockerfile.cuda128 | 8 +------- docker/dockerfile.grpc | 6 +----- grpc_server.py | 9 ++++++--- 3 files changed, 8 insertions(+), 15 deletions(-) mode change 100644 => 100755 grpc_server.py diff --git a/docker/dockerfile.cuda128 b/docker/dockerfile.cuda128 index 59939842c..d56c28157 100644 --- a/docker/dockerfile.cuda128 +++ b/docker/dockerfile.cuda128 @@ -4,16 +4,10 @@ # (unchanged from your original dockerfile.grpc) # ============================================================================= FROM python:3.11-slim AS protogen -# grpcio-tools==1.68.0's bundled protoc always stamps generated _pb2.py gencode as -# protobuf 5.28.1 - that's baked into the wheel itself and does NOT depend on which -# protobuf package version gets installed alongside it here. What actually matters is -# the runtime protobuf version installed where the generated code executes (pinned in -# futur_grpc's pyproject.toml), which must be >= 5.28.1 per protobuf's version guarantee. -# Pinned here only for consistency with that runtime pin, not because it affects gencode. RUN pip install --no-cache-dir grpcio-tools==1.68.0 RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /root/.ssh/known_hosts -RUN --mount=type=ssh git clone --depth 1 --branch v1.1.2 \ +RUN --mount=type=ssh git clone --depth 1 --branch v1.1.3 \ git@github.com:FuturHandRobotics/futur_grpc.git /proto-src WORKDIR /proto-src RUN chmod +x scripts/generate.sh && ./scripts/generate.sh diff --git a/docker/dockerfile.grpc b/docker/dockerfile.grpc index 353b615c7..ee3e0673a 100644 --- a/docker/dockerfile.grpc +++ b/docker/dockerfile.grpc @@ -1,14 +1,10 @@ # Intermediate build image which compiles the gRPC commands. # Goes at the top since Docker uses the final FROM as the resulting image FROM python:3.11-slim AS protogen -# Pin protobuf here to match futur_grpc's pyproject.toml runtime pin (protobuf==5.28.0). -# Otherwise pip resolves grpcio-tools' loose protobuf constraint independently (currently 5.28.1), -# and generate.sh stamps the generated _pb2.py gencode with that newer version, which is then -# incompatible with the older protobuf runtime that futur_grpc actually installs downstream. RUN pip install --no-cache-dir grpcio-tools==1.68.0 RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /root/.ssh/known_hosts -RUN --mount=type=ssh git clone --depth 1 --branch v1.1.2 \ +RUN --mount=type=ssh git clone --depth 1 --branch v1.1.3 \ git@github.com:FuturHandRobotics/futur_grpc.git /proto-src WORKDIR /proto-src RUN chmod +x scripts/generate.sh && ./scripts/generate.sh diff --git a/grpc_server.py b/grpc_server.py old mode 100644 new mode 100755 index 65f0044fb..26f2c783b --- a/grpc_server.py +++ b/grpc_server.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 from estimater import * import argparse from concurrent import futures @@ -20,7 +21,7 @@ def __init__(self, est, est_refine_iter, track_refine_iter): def _unpack_frame(self, request): rgb = np.frombuffer(request.rgb_data, dtype=np.uint8).reshape(request.height, request.width, 3) depth = np.frombuffer(request.depth_data, dtype=np.float32).reshape(request.height, request.width) - K = np.array(request.intrinsics, dtype=np.float32).reshape(3, 3) + K = np.array(request.intrinsics, dtype=np.float64).reshape(3, 3) return rgb, depth, K def TrackStream(self, request_iterator, context): @@ -42,6 +43,8 @@ def TrackStream(self, request_iterator, context): rotation=pose[:3, :3].reshape(-1).tolist(), success=True, ) + #Reset the stream to prevent stale state from hanging around + self.est.pose_last = None def Reset(self, request, context): self.est.pose_last = None @@ -58,8 +61,8 @@ def SeedPose(self, request, context): if __name__ == '__main__': code_dir = os.path.dirname(os.path.realpath(__file__)) parser = argparse.ArgumentParser() - parser.add_argument('mesh_file', type=str, help='path to the object mesh to load (e.g. textured_simple.obj)',\ - default='{code_dir}/demo_data/mustard0/mesh/textured_simple.obj') + parser.add_argument('mesh_file', type=str, nargs='?', help='path to the object mesh to load (e.g. textured_simple.obj)', + default=f'{code_dir}/demo_data/mustard0/mesh/textured_simple.obj') parser.add_argument('--port', type=int, default=50051) parser.add_argument('--est_refine_iter', type=int, default=5) parser.add_argument('--track_refine_iter', type=int, default=2) From 7ca02ce9e4bdbf5279c68562b27242b918116ef1 Mon Sep 17 00:00:00 2001 From: Theo Coulson Date: Wed, 22 Jul 2026 17:19:53 -0400 Subject: [PATCH 09/10] mesh texture handling --- grpc_server.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/grpc_server.py b/grpc_server.py index 26f2c783b..ed1d47076 100755 --- a/grpc_server.py +++ b/grpc_server.py @@ -62,7 +62,7 @@ def SeedPose(self, request, context): code_dir = os.path.dirname(os.path.realpath(__file__)) parser = argparse.ArgumentParser() parser.add_argument('mesh_file', type=str, nargs='?', help='path to the object mesh to load (e.g. textured_simple.obj)', - default=f'{code_dir}/demo_data/mustard0/mesh/textured_simple.obj') + default=f'{code_dir}/meshes/Perfectionist_Pro_Puck.obj') parser.add_argument('--port', type=int, default=50051) parser.add_argument('--est_refine_iter', type=int, default=5) parser.add_argument('--track_refine_iter', type=int, default=2) @@ -75,6 +75,19 @@ def SeedPose(self, request, context): mesh = trimesh.load(args.mesh_file) + if mesh.visual.kind == 'texture' and mesh.visual.material.image is None: + try: + color = mesh.visual.material.diffuse[:3] + except Exception: + color = [128, 128, 128] + tex = Image.new('RGB', (16, 16), tuple(int(c) for c in color)) + mesh.visual.material.image = tex + + if mesh.visual.uv is None: + # dummy UVs — since the texture is a flat solid color, it doesn't matter + # what they point to, they just need to exist and be the right shape + mesh.visual.uv = np.zeros((len(mesh.vertices), 2), dtype=np.float32) + scorer = ScorePredictor() refiner = PoseRefinePredictor() glctx = dr.RasterizeCudaContext() From c1d0665eaf28fdace7185cb45cea9e697dedda82 Mon Sep 17 00:00:00 2001 From: Theo Coulson Date: Mon, 27 Jul 2026 17:48:24 -0400 Subject: [PATCH 10/10] increase the maximum accepted image size --- grpc_server.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/grpc_server.py b/grpc_server.py index ed1d47076..93538d0ff 100755 --- a/grpc_server.py +++ b/grpc_server.py @@ -103,7 +103,13 @@ def SeedPose(self, request, context): ) logging.info('estimator initialization done') - server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=10), + options=[ + ('grpc.max_send_message_length', 64 * 1024 * 1024), + ('grpc.max_receive_message_length', 64 * 1024 * 1024), + ], + ) add_PoseEstimateServicer_to_server( PoseEstimateService(est, args.est_refine_iter, args.track_refine_iter), server )