From ba7093d2688dac1dd6fc5254220e97664adffdbf Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 26 Aug 2026 13:14:59 -0400 Subject: [PATCH] fix(release): mirror the skills repo the way ai-plugin syncs The mirror step committed straight to the target's default branch through createCommitOnBranch, which takes the whole tree in one request and times out once that tree is large. Each failure left more drift for the next release, so PostHog/skills has been stuck at v1.9.4 since March. Lands the sync the way PostHog/ai-plugin's own sync-skills workflow does instead: push a branch whose tip is the base commit, have the API create the signed commit on it, open a PR, let auto-merge squash it in. The mirror never writes to the default branch directly, and a failed run leaves a branch rather than a half-synced main. The mirror moves into a composite action, matching the mirror-to-s3 action already in this repo. Also invokes the omnibus hook through bash, since the mirrored copy has no executable bit. Co-Authored-By: Claude Fable 5 --- .github/actions/sync-skills/action.yml | 107 +++++++++++++++++++++++++ .github/workflows/build-release.yml | 63 ++------------- scripts/lib/marketplace-generator.js | 7 +- 3 files changed, 118 insertions(+), 59 deletions(-) create mode 100644 .github/actions/sync-skills/action.yml diff --git a/.github/actions/sync-skills/action.yml b/.github/actions/sync-skills/action.yml new file mode 100644 index 00000000..70985933 --- /dev/null +++ b/.github/actions/sync-skills/action.yml @@ -0,0 +1,107 @@ +name: "Mirror plugins to the skills repo" +description: >- + Sync the built marketplace plugins into PostHog/skills on a branch, then + open an auto-merging PR. + +# Called by build-release.yml after the GitHub release is published. +# +# The target repo requires signed commits and changes via PR, so this mirrors +# what PostHog/ai-plugin's own sync does: push a branch, have the GitHub API +# create the commit on it (the API signs what it creates), open a PR, and let +# auto-merge land it. +# +# The caller is responsible for checkout, Node/pnpm setup, `pnpm install`, and +# `pnpm run build` — this action assumes dist/ already exists. + +inputs: + token: + description: "App token with write access to the target repo" + required: true + message: + description: "Commit message" + required: true + +outputs: + repo: + description: "The target repo, from push-manifest.json" + value: ${{ steps.sync.outputs.repo }} + branch: + description: "The sync branch that was pushed, empty when nothing changed" + value: ${{ steps.sync.outputs.branch }} + base: + description: "The branch the PR targets" + value: ${{ steps.sync.outputs.base }} + +runs: + using: composite + steps: + - id: sync + shell: bash + env: + TOKEN: ${{ inputs.token }} + MESSAGE: ${{ inputs.message }} + run: | + MANIFEST="dist/push-manifest.json" + TARGET_REPO=$(jq -r '.target_repo' "$MANIFEST") + # ghcommit-action resolves its `repository` input under $GITHUB_WORKSPACE. + WORK_DIR="$GITHUB_WORKSPACE/.skills-mirror" + rm -rf "$WORK_DIR" + + echo "Cloning $TARGET_REPO..." + git clone --quiet "https://x-access-token:${TOKEN}@github.com/${TARGET_REPO}.git" "$WORK_DIR" + + jq -c '.plugins[]' "$MANIFEST" | while read -r entry; do + NAME=$(echo "$entry" | jq -r '.name') + SOURCE="$PWD/dist/$(echo "$entry" | jq -r '.source')" + DEST="$WORK_DIR/$(echo "$entry" | jq -r '.destination')" + echo " $NAME -> $(echo "$entry" | jq -r '.destination')" + if [ -d "$DEST" ]; then + find "$DEST" -mindepth 1 -not -name 'README.md' -exec rm -rf {} + 2>/dev/null || true + fi + mkdir -p "$DEST" + cp -r "$SOURCE"/. "$DEST"/ + done + + cd "$WORK_DIR" + git add -A + if git diff --cached --quiet; then + echo "Already up to date." + exit 0 + fi + + # The branch tip is the base commit, so this push creates no new + # (unsigned) commits; ghcommit-action lays the signed one on top. + BASE=$(git rev-parse --abbrev-ref HEAD) + BRANCH="chore/sync-plugins-$(date +%Y%m%d-%H%M%S)" + git push --quiet origin "HEAD:refs/heads/$BRANCH" + { + echo "repo=$TARGET_REPO" + echo "base=$BASE" + echo "branch=$BRANCH" + } >> "$GITHUB_OUTPUT" + + - if: steps.sync.outputs.branch != '' + uses: planetscale/ghcommit-action@a6b150b81dca5dd027baa898604418eec9e11465 # v0.2.22 + with: + commit_message: ${{ inputs.message }} + repo: ${{ steps.sync.outputs.repo }} + branch: ${{ steps.sync.outputs.branch }} + repository: .skills-mirror + env: + GITHUB_TOKEN: ${{ inputs.token }} + + - if: steps.sync.outputs.branch != '' + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + TARGET_REPO: ${{ steps.sync.outputs.repo }} + BASE: ${{ steps.sync.outputs.base }} + BRANCH: ${{ steps.sync.outputs.branch }} + MESSAGE: ${{ inputs.message }} + run: | + PR_URL=$(gh pr create --repo "$TARGET_REPO" \ + --base "$BASE" --head "$BRANCH" \ + --title "$MESSAGE" \ + --body "Automated plugin sync from context-mill.") + echo "Opened $PR_URL" + gh pr merge --repo "$TARGET_REPO" "$PR_URL" --auto --squash diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 2df164c8..05ad4c07 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -266,64 +266,11 @@ jobs: private-key: ${{ secrets.SKILLS_PUSH_APP_PRIVATE_KEY }} repositories: skills,ai-plugin - - name: Push plugins to skills repo - id: skills_push - env: - SKILLS_REPO_TOKEN: ${{ steps.skills_token.outputs.token }} - run: | - MANIFEST="dist/push-manifest.json" - TARGET_REPO=$(jq -r '.target_repo' "$MANIFEST") - # ghcommit-action resolves its `repository` input under $GITHUB_WORKSPACE, - # so the clone lives there rather than in a mktemp dir. - WORK_DIR="$GITHUB_WORKSPACE/.skills-push" - rm -rf "$WORK_DIR" - - echo "Cloning $TARGET_REPO..." - git clone "https://x-access-token:${SKILLS_REPO_TOKEN}@github.com/${TARGET_REPO}.git" "$WORK_DIR" - { - echo "target_repo=$TARGET_REPO" - echo "target_branch=$(git -C "$WORK_DIR" rev-parse --abbrev-ref HEAD)" - } >> "$GITHUB_OUTPUT" - - # Read each plugin entry from the push manifest and sync to the destination - jq -c '.plugins[]' "$MANIFEST" | while read -r entry; do - NAME=$(echo "$entry" | jq -r '.name') - SOURCE="dist/$(echo "$entry" | jq -r '.source')" - DEST="$WORK_DIR/$(echo "$entry" | jq -r '.destination')" - - echo " Syncing $NAME → $(echo "$entry" | jq -r '.destination')" - - # Clear destination (preserve README.md if present) - if [ -d "$DEST" ]; then - find "$DEST" -mindepth 1 -not -name 'README.md' -not -name '.' -exec rm -rf {} + 2>/dev/null || true - fi - mkdir -p "$DEST" - - # Copy plugin contents - cp -r "$SOURCE"/. "$DEST"/ - done - - cd "$WORK_DIR" - git add -A - if git diff --cached --quiet; then - echo "No changes to push" - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - # Both target repos require signed commits, so the mirrored content is - # committed through the GitHub API instead of pushed with git. - - name: Commit plugins to skills repo - if: steps.skills_push.outputs.changed == 'true' - uses: planetscale/ghcommit-action@a6b150b81dca5dd027baa898604418eec9e11465 # v0.2.22 + - name: Mirror plugins to the skills repo + uses: ./.github/actions/sync-skills with: - commit_message: Update generated plugins from context-mill ${{ env.RELEASE_TAG }} - repo: ${{ steps.skills_push.outputs.target_repo }} - branch: ${{ steps.skills_push.outputs.target_branch }} - repository: .skills-push - env: - GITHUB_TOKEN: ${{ steps.skills_token.outputs.token }} + token: ${{ steps.skills_token.outputs.token }} + message: Update generated plugins from context-mill ${{ env.RELEASE_TAG }} - name: Push omnibus skills to ai-plugin repo id: ai_plugin_push @@ -375,7 +322,7 @@ jobs: # workspace rather than a mktemp dir, so drop them before any later step runs. - name: Remove the mirror clones if: always() - run: rm -rf "$GITHUB_WORKSPACE/.skills-push" "$GITHUB_WORKSPACE/.ai-plugin-push" + run: rm -rf "$GITHUB_WORKSPACE/.skills-mirror" "$GITHUB_WORKSPACE/.ai-plugin-push" # Mirror the release to S3 so consumers have a second origin to fetch from # when GitHub Releases is unavailable. diff --git a/scripts/lib/marketplace-generator.js b/scripts/lib/marketplace-generator.js index 74ca1ec6..b22a3e34 100644 --- a/scripts/lib/marketplace-generator.js +++ b/scripts/lib/marketplace-generator.js @@ -96,8 +96,13 @@ function writeSkillReminderHook(pluginDir) { { hooks: [ { + // Invoked through bash so the hook survives losing + // its executable bit: the plugin trees reach + // PostHog/skills as GitHub-API-signed commits + // (createCommitOnBranch), and that API has no file + // mode support — everything lands as 100644. type: 'command', - command: '${CLAUDE_PLUGIN_ROOT}/hooks/skill-reminder.sh', + command: 'bash ${CLAUDE_PLUGIN_ROOT}/hooks/skill-reminder.sh', }, ], },