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', }, ], },