From c79ba126fd58ebb2c357022fda20a98d2bb6ca8a Mon Sep 17 00:00:00 2001 From: Spicrawl <335905600+Spicrawl@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:41:40 +0530 Subject: [PATCH] Auto-release all plugins on merge to main Adds .github/workflows/release-all-on-merge.yml, which runs on every push to main. For each supported plugin format it reads the manifest version, skips any version already released, and otherwise builds the artifact and creates the GitHub release. Jobs run in parallel so a failure in one plugin release does not block the others. Tag-based workflows remain as a manual fallback. Also updates README.md to document the new auto-release behavior. --- .github/workflows/release-all-on-merge.yml | 344 +++++++++++++++++++++ README.md | 6 +- 2 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release-all-on-merge.yml diff --git a/.github/workflows/release-all-on-merge.yml b/.github/workflows/release-all-on-merge.yml new file mode 100644 index 0000000..706738d --- /dev/null +++ b/.github/workflows/release-all-on-merge.yml @@ -0,0 +1,344 @@ +# Automatically releases every plugin format when a PR is merged to main. +# +# Each plugin has its own job. A job checks whether a release for the current +# manifest version already exists; if not, it builds the artifact and creates +# the GitHub release. Jobs run in parallel, so a failure in one plugin release +# does not block the others. +# +# This means: after bumping versions in a PR and merging to main, every plugin +# that has a new version will be published without manually pushing tags. +# +# Tag-based workflows (release-*.yml, release-all.yml) remain available as a +# manual fallback. + +name: release-all-on-merge + +on: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: release-all-on-merge-${{ github.ref }} + cancel-in-progress: false + +env: + DIFY_CLI_VERSION: 0.6.10 + DIFY_CLI_SHA256: 0cef74bcae375a4337c2ff7d42e4787717981a795e1c23cf56bb27ec07ec8304 + +jobs: + openai: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release OpenAI plugin + env: + GH_TOKEN: ${{ github.token }} + OPENAI_APP_ID: ${{ secrets.OPENAI_APP_ID }} + run: | + version="$(jq -r '.version' plugins/spicrawl-openai/plugin.json)" + tag="openai-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "OpenAI release $tag already exists; skipping" + exit 0 + fi + if [ -z "$OPENAI_APP_ID" ]; then + echo "::error::the OPENAI_APP_ID repository secret is not set" + exit 1 + fi + jq '.apps.spicrawl.id = env.OPENAI_APP_ID' plugins/spicrawl-openai/.app.json > "$RUNNER_TEMP/.app.json" + mv "$RUNNER_TEMP/.app.json" plugins/spicrawl-openai/.app.json + cd plugins/spicrawl-openai + zip -r "$RUNNER_TEMP/spicrawl-openai-$version.zip" . + cd ../.. + gh release create "$tag" "$RUNNER_TEMP/spicrawl-openai-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for OpenAI $version." + + cursor: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release Cursor plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' plugins/spicrawl/.cursor-plugin/plugin.json)" + tag="cursor-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Cursor release $tag already exists; skipping" + exit 0 + fi + cd plugins/spicrawl + zip -r "$RUNNER_TEMP/spicrawl-cursor-$version.zip" . + cd ../.. + gh release create "$tag" "$RUNNER_TEMP/spicrawl-cursor-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for Cursor $version." + + claude: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release Claude Code plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' plugins/spicrawl/.claude-plugin/plugin.json)" + tag="claude-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Claude release $tag already exists; skipping" + exit 0 + fi + cd plugins/spicrawl + zip -r "$RUNNER_TEMP/spicrawl-claude-$version.zip" . + cd ../.. + gh release create "$tag" "$RUNNER_TEMP/spicrawl-claude-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for Claude Code $version." + + factory: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release Factory Droid plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' plugins/spicrawl/.factory-plugin/plugin.json)" + tag="factory-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Factory release $tag already exists; skipping" + exit 0 + fi + cd plugins/spicrawl + zip -r "$RUNNER_TEMP/spicrawl-factory-$version.zip" . + cd ../.. + gh release create "$tag" "$RUNNER_TEMP/spicrawl-factory-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for Factory Droid $version." + + devin: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release Devin plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' plugins/spicrawl/.claude-plugin/plugin.json)" + tag="devin-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Devin release $tag already exists; skipping" + exit 0 + fi + cd plugins/spicrawl + zip -r "$RUNNER_TEMP/spicrawl-devin-$version.zip" . + cd ../.. + gh release create "$tag" "$RUNNER_TEMP/spicrawl-devin-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for Devin $version." + + copilot: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release GitHub Copilot CLI plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' plugins/spicrawl/.github/plugin/plugin.json)" + tag="copilot-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Copilot release $tag already exists; skipping" + exit 0 + fi + cd plugins/spicrawl + zip -r "$RUNNER_TEMP/spicrawl-copilot-$version.zip" . + cd ../.. + gh release create "$tag" "$RUNNER_TEMP/spicrawl-copilot-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for GitHub Copilot CLI $version." + + gemini: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release Gemini CLI plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' gemini-extension.json)" + tag="gemini-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Gemini release $tag already exists; skipping" + exit 0 + fi + zip -r "$RUNNER_TEMP/spicrawl-gemini-$version.zip" gemini-extension.json GEMINI.md LICENSE README.md + gh release create "$tag" "$RUNNER_TEMP/spicrawl-gemini-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl extension for Gemini CLI $version." + + openbot: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: release OpenBot plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' openbot/plugin.json)" + tag="openbot-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "OpenBot release $tag already exists; skipping" + exit 0 + fi + zip -r "$RUNNER_TEMP/spicrawl-openbot-$version.zip" openbot/plugin.json openbot/README.md + gh release create "$tag" "$RUNNER_TEMP/spicrawl-openbot-$version.zip" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for OpenBot $version." + + dify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: install the Dify CLI + run: | + curl -fsSL -o "$RUNNER_TEMP/dify-plugin" \ + "https://github.com/langgenius/dify-plugin-daemon/releases/download/$DIFY_CLI_VERSION/dify-plugin-linux-amd64" + echo "$DIFY_CLI_SHA256 $RUNNER_TEMP/dify-plugin" | sha256sum -c - + chmod +x "$RUNNER_TEMP/dify-plugin" + - name: release Dify plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(awk '/^version:/ { print $2; exit }' dify/manifest.yaml)" + tag="dify-v$version" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Dify release $tag already exists; skipping" + exit 0 + fi + "$RUNNER_TEMP/dify-plugin" plugin package dify -o "$RUNNER_TEMP/spicrawl-$version.difypkg" + gh release create "$tag" "$RUNNER_TEMP/spicrawl-$version.difypkg" \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugin for Dify $version." + + opencode: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: write + id-token: write + defaults: + run: + working-directory: opencode + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: opencode/package-lock.json + registry-url: https://registry.npmjs.org + - name: install + run: npm ci --no-audit --no-fund + - name: typecheck + run: npm run check + - name: test + run: npm test + - name: publish to npm and create GitHub release + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GH_TOKEN: ${{ github.token }} + run: | + name="$(node -p 'require("./package.json").name')" + version="$(node -p 'require("./package.json").version')" + if npm view "$name@$version" version >/dev/null 2>&1; then + echo "$name@$version is already published; skipping" + exit 0 + fi + if [ -z "$NODE_AUTH_TOKEN" ]; then + echo "::error::the NPM_TOKEN repository secret is not set" + exit 1 + fi + tag=latest + case "$version" in *-*) tag=next ;; esac + npm publish --provenance --access public --tag "$tag" + ghtag="opencode-spicrawl-v$version" + awk -v v="$version" 'index($0, "## " v " ") == 1 { f = 1; next } f && /^## / { exit } f' \ + CHANGELOG.md > "$RUNNER_TEMP/notes.md" + [ -s "$RUNNER_TEMP/notes.md" ] || echo "Published opencode-spicrawl@$version to npm." > "$RUNNER_TEMP/notes.md" + flags=() + case "$version" in *-*) flags+=(--prerelease) ;; esac + gh release create "$ghtag" --target "$GITHUB_SHA" --title "$ghtag" \ + --notes-file "$RUNNER_TEMP/notes.md" "${flags[@]}" + + unified: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + needs: [openai, cursor, claude, factory, devin, copilot, gemini, openbot, dify, opencode] + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: create or update unified v* release + env: + GH_TOKEN: ${{ github.token }} + run: | + version="$(jq -r '.version' plugins/spicrawl-openai/plugin.json)" + tag="v$version" + gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1 && { + echo "Unified release $tag already exists; skipping" + exit 0 + } + # Collect artifacts from all per-format releases for this version. + # We download them from the existing releases and re-attach them to + # a single v* release page. + mkdir -p "$RUNNER_TEMP/assets" + for suffix in openai cursor claude factory devin copilot gemini openbot; do + asset="spicrawl-$suffix-$version.zip" + [ "$suffix" = "openai" ] && asset="spicrawl-openai-$version.zip" + release_tag="$suffix-v$version" + gh release download "$release_tag" --repo "${{ github.repository }}" \ + --pattern "$asset" --dir "$RUNNER_TEMP/assets" 2>/dev/null || true + done + # Dify asset + gh release download "dify-v$version" --repo "${{ github.repository }}" \ + --pattern "spicrawl-$version.difypkg" --dir "$RUNNER_TEMP/assets" 2>/dev/null || true + # OpenCode asset (no file asset, just the npm release) + if [ "$(ls -A "$RUNNER_TEMP/assets")" ]; then + gh release create "$tag" "$RUNNER_TEMP/assets"/* \ + --target "$GITHUB_SHA" --title "$tag" \ + --notes "Spicrawl plugins release $version." + else + echo "No assets found to attach to unified release $tag" + fi diff --git a/README.md b/README.md index 0a7e6a0..e38e406 100644 --- a/README.md +++ b/README.md @@ -550,7 +550,9 @@ The Dify plugin (`dify/`) is released by `.github/workflows/release-dify.yml`. 1. Bump `version` in `dify/manifest.yaml` (the `meta.version` field is the manifest format version; leave it). 2. Merge to `main`, then push the tag `dify-vX.Y.Z` on the merged commit. The tag must match the manifest. The workflow packages `spicrawl-X.Y.Z.difypkg` with the Dify plugin CLI (pinned to one release and checked against its SHA-256) and attaches it to a GitHub release of that tag. -Every plugin format now has a release workflow that builds a ZIP and attaches it to a GitHub release. Push the tag shown in the workflow to trigger it. +Plugin releases happen automatically when a version-bumping PR is merged to `main`. The `.github/workflows/release-all-on-merge.yml` workflow reads each manifest version, skips any plugin that is already released, and builds/uploads the rest. + +You can still trigger releases manually with tags: | Plugin / tool | Workflow | Trigger tag | Artifact | |---|---|---|---| @@ -564,6 +566,6 @@ Every plugin format now has a release workflow that builds a ZIP and attaches it | OpenBot | `.github/workflows/release-openbot.yml` | `openbot-v*` | `spicrawl-openbot-.zip` | | All of the above (one release page) | `.github/workflows/release-all.yml` | `v*` | all of the above | -The OpenAI workflow requires the `OPENAI_APP_ID` repository secret, which CI injects into `.app.json` before packaging. Until that secret is set, the workflow fails. The registered app ID comes from the OpenAI portal when the Spicrawl app is registered. +The OpenAI workflow requires the `OPENAI_APP_ID` repository secret, which CI injects into `.app.json` before packaging. The registered app ID comes from the OpenAI portal when the Spicrawl app is registered. The `validate` workflow runs on every push and pull request. It parses all JSON files, runs the OpenCode tests, runs the Dify plugin's pytest, and fails if any file contains something matching `spicrawl_(live|test)_[a-z0-9]{20,}`.