diff --git a/.github/workflows/downstream-release-dispatch.yml b/.github/workflows/downstream-release-dispatch.yml new file mode 100644 index 0000000..4e17697 --- /dev/null +++ b/.github/workflows/downstream-release-dispatch.yml @@ -0,0 +1,83 @@ +name: Downstream Release Dispatch + +on: + release: + types: [published] + +concurrency: + group: downstream-release-dispatch-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +env: + CONSUMER_REPOS: | + microsoft/vscode-isort + microsoft/vscode-flake8 + microsoft/vscode-pylint + microsoft/vscode-mypy + microsoft/vscode-black-formatter + NPM_DEPENDENCY: "@vscode/common-python-lsp" + PIP_DEPENDENCY: "vscode-common-python-lsp" + +jobs: + dispatch-to-consumers: + name: Dispatch Release Event To Consumer Repos + runs-on: ubuntu-latest + steps: + - name: Send repository_dispatch to each consumer repo + env: + GH_TOKEN: ${{ secrets.DOWNSTREAM_REPO_TOKEN }} + SOURCE_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_URL: ${{ github.event.release.html_url }} + RELEASE_NAME: ${{ github.event.release.name || '' }} + run: | + set -euo pipefail + + if [ -z "${GH_TOKEN:-}" ]; then + echo "::error::DOWNSTREAM_REPO_TOKEN is required for cross-repo dispatch" + exit 1 + fi + + failures="" + + for repo in $CONSUMER_REPOS; do + echo "Dispatching shared-package-release event to $repo" + + payload=$(jq -n \ + --arg source_repo "$SOURCE_REPO" \ + --arg release_tag "$RELEASE_TAG" \ + --arg release_url "$RELEASE_URL" \ + --arg release_name "$RELEASE_NAME" \ + --arg npm_dependency "$NPM_DEPENDENCY" \ + --arg pip_dependency "$PIP_DEPENDENCY" \ + '{ + event_type: "shared-package-release", + client_payload: { + source_repo: $source_repo, + release_tag: $release_tag, + release_url: $release_url, + release_name: $release_name, + npm_dependency: $npm_dependency, + pip_dependency: $pip_dependency + } + }') + + if ! gh api "/repos/$repo/dispatches" \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + --input - <<< "$payload"; then + echo "Dispatch failed for $repo" + failures+="$repo "$'\n' + continue + fi + done + + if [ -n "$failures" ]; then + echo "Dispatch failed for the following repositories:" + echo "$failures" + exit 1 + fi diff --git a/docs/consumer-repo-dispatch-template.md b/docs/consumer-repo-dispatch-template.md new file mode 100644 index 0000000..4fa6eb3 --- /dev/null +++ b/docs/consumer-repo-dispatch-template.md @@ -0,0 +1,120 @@ +# Consumer Repo Workflow Template + +Use this workflow in each consumer repository to handle `repository_dispatch` events sent by: +- [.github/workflows/downstream-release-dispatch.yml](../.github/workflows/downstream-release-dispatch.yml) + +```yaml +name: Shared Package Release Handler + +on: + repository_dispatch: + types: [shared-package-release] + +permissions: + contents: write + pull-requests: write + +jobs: + update-shared-packages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "20" + + - uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Create branch for this release + env: + RELEASE_TAG: ${{ github.event.client_payload.release_tag }} + run: | + set -euo pipefail + BRANCH="shared-package-v${RELEASE_TAG#v}" + git fetch origin main + git checkout -B "$BRANCH" origin/main + + - name: Normalize release tag + id: release_version + env: + RELEASE_TAG: ${{ github.event.client_payload.release_tag }} + run: | + set -euo pipefail + VERSION="${RELEASE_TAG#v}" + echo "value=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Update npm dependency + env: + NPM_DEP: ${{ github.event.client_payload.npm_dependency }} + RELEASE_VERSION: ${{ steps.release_version.outputs.value }} + run: | + set -euo pipefail + npm install "$NPM_DEP@$RELEASE_VERSION" + + - name: Update pip dependency reference (example: requirements file) + env: + PIP_DEP: ${{ github.event.client_payload.pip_dependency }} + RELEASE_VERSION: ${{ steps.release_version.outputs.value }} + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + import os + import re + + dep = os.environ["PIP_DEP"] + version = os.environ["RELEASE_VERSION"] + pinned = f"{dep}=={version}" + req = Path("requirements.txt") + if req.exists(): + txt = req.read_text(encoding="utf-8") + updated = re.sub(rf"^{re.escape(dep)}([<>=!~].*)?$", pinned, txt, flags=re.MULTILINE) + req.write_text(updated, encoding="utf-8") + PY + + - name: Commit and open PR if changed + env: + RELEASE_TAG: ${{ github.event.client_payload.release_tag }} + RELEASE_URL: ${{ github.event.client_payload.release_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if git diff --quiet; then + echo "No changes detected." + exit 0 + fi + + BRANCH="shared-package-v${RELEASE_TAG#v}" + git add -A + git commit -m "Upgrade shared package to ${RELEASE_TAG}" + git push --set-upstream origin "$BRANCH" + + gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "[Shared Package] Upgrade to ${RELEASE_TAG}" \ + --body "Automated update from ${RELEASE_URL}" +``` + +## Dispatch payload fields + +The dispatcher sends these `client_payload` fields: +- `source_repo` +- `release_tag` +- `release_url` +- `release_name` +- `npm_dependency` +- `pip_dependency` + +## Template notes + +- This is a starter template. Each consumer repo should adjust file paths and update commands to match its actual layout. +- The `Normalize release tag` step strips a single leading `v` (for example, `v1.2.3` -> `1.2.3`) and the npm/pip steps pin to that normalized version. +- The pip step must update tracked files (for example `requirements.txt`, `pyproject.toml`, or lock files), not just install locally.