Release #68
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| type: | |
| description: 'Release type' | |
| required: true | |
| type: choice | |
| options: | |
| - pre-release | |
| - release | |
| version: | |
| description: 'Version bump' | |
| required: false | |
| type: choice | |
| default: patch | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| skip-windows-e2e: | |
| description: 'Skip Windows E2E tests (they take a long time)' | |
| required: false | |
| type: boolean | |
| default: false | |
| skip-macos-e2e: | |
| description: 'Skip macOS E2E tests (they take a long time)' | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| id-token: write | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| e2e-windows: | |
| if: ${{ !inputs.skip-windows-e2e }} | |
| uses: ./.github/workflows/e2e-windows.yml | |
| e2e-macos: | |
| if: ${{ !inputs.skip-macos-e2e }} | |
| uses: ./.github/workflows/e2e-macos.yml | |
| e2e-linux: | |
| # Re-run the full Linux E2E matrix on the exact commit being released. | |
| # ci.yml only runs on push/PR, so a manually-dispatched release (especially | |
| # a pre-release from a feature branch) is not otherwise guaranteed to have | |
| # had Linux E2E executed against this SHA. | |
| uses: ./.github/workflows/e2e-linux.yml | |
| conformance: | |
| # Gate releases on MCP spec conformance so we never publish a version | |
| # that regresses against the reference conformance suite. | |
| uses: ./.github/workflows/conformance.yml | |
| release: | |
| name: Publish ${{ inputs.type }} | |
| runs-on: ubuntu-latest | |
| needs: [e2e-windows, e2e-macos, e2e-linux, conformance] | |
| # Release must be from main; pre-release can be from any branch. | |
| # Allow the e2e-windows and e2e-macos dependencies to be either successful | |
| # or skipped (when the user opts out via skip-windows-e2e / skip-macos-e2e). | |
| # Linux E2E and conformance must pass. | |
| if: | | |
| always() && | |
| (needs.e2e-windows.result == 'success' || needs.e2e-windows.result == 'skipped') && | |
| (needs.e2e-macos.result == 'success' || needs.e2e-macos.result == 'skipped') && | |
| needs.e2e-linux.result == 'success' && | |
| needs.conformance.result == 'success' && | |
| (inputs.type == 'pre-release' || github.ref == 'refs/heads/main') | |
| steps: | |
| - name: Validate inputs | |
| run: | | |
| if [[ "${{ inputs.type }}" == "release" && "${{ github.ref }}" != "refs/heads/main" ]]; then | |
| echo "::error::Releases must be published from the main branch." | |
| exit 1 | |
| fi | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| # Use a PAT so the release job can push the version-bump | |
| # commit and tag directly to main, bypassing branch-protection | |
| # rulesets that block the default GITHUB_TOKEN. | |
| token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| registry-url: https://registry.npmjs.org | |
| - name: Install pnpm and dependencies | |
| uses: apify/actions/pnpm-install@v1.1.2 | |
| - name: Check dependency age | |
| # Supply-chain gate: pnpm's `minimumReleaseAge` only applies when resolving new | |
| # versions, and pnpm 10 does not re-check an existing lockfile on a | |
| # --frozen-lockfile install, so without this nothing enforces the quarantine at | |
| # release time. Runs before the version bump, tag, push and publish steps, so a | |
| # failure costs nothing and leaves no partial release behind. | |
| run: pnpm run check:deps-age | |
| - name: Lint | |
| run: pnpm run lint | |
| - name: Build | |
| run: pnpm run build | |
| - name: Unit tests | |
| run: pnpm run test:unit | |
| - name: Update README | |
| if: inputs.type == 'release' | |
| run: pnpm run build:readme | |
| - name: Update REFERENCE.md | |
| if: inputs.type == 'release' | |
| run: pnpm run build:reference | |
| - name: Report install size | |
| id: install-size | |
| # Measures the tarball/unpacked/full-install size of what will be | |
| # published and compares it with the latest version on npm: prints a | |
| # table to the logs and run summary, warns if this release is much | |
| # bigger, and exposes the report as an output that the GitHub release | |
| # steps embed in the release body. | |
| run: node scripts/report-install-size.mjs | |
| # ── Pre-release ──────────────────────────────────────────────── | |
| - name: Bump pre-release version | |
| if: inputs.type == 'pre-release' | |
| id: prerelease | |
| run: | | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| CURRENT=$(node -p "require('./package.json').version") | |
| BUMP="${{ inputs.version }}" | |
| # Compute the next base version according to the selected bump type | |
| # e.g. 0.1.10 + minor -> 0.2.0, 0.1.10 + major -> 1.0.0, 0.1.10 + patch -> 0.1.11 | |
| # If already a beta, strip the pre-release suffix first | |
| NEXT_BASE=$(node -p " | |
| const base = '${CURRENT}'.replace(/-beta\..*/, ''); | |
| const v = base.split('.').map(Number); | |
| if ('${BUMP}' === 'major') { v[0]++; v[1] = 0; v[2] = 0; } | |
| else if ('${BUMP}' === 'minor') { v[1]++; v[2] = 0; } | |
| else { v[2]++; } | |
| v.join('.') | |
| ") | |
| # If current version is already a beta for this base, don't re-bump | |
| CURRENT_BASE=$(node -p "'${CURRENT}'.replace(/-beta\..*/, '')") | |
| if [[ "${CURRENT}" == *"-beta."* ]]; then | |
| # Already on a beta — check if it matches the target base | |
| # e.g. current=0.2.0-beta.1, bump=minor from 0.1.10 -> NEXT_BASE=0.2.0 -> matches, keep it | |
| # e.g. current=0.1.11-beta.1, bump=minor from 0.1.10 -> NEXT_BASE=0.2.0 -> mismatch, use NEXT_BASE | |
| if [[ "$CURRENT_BASE" == "$NEXT_BASE" ]]; then | |
| NEXT_BASE="$CURRENT_BASE" | |
| fi | |
| fi | |
| echo "Base version for beta: $NEXT_BASE" | |
| # Query npm for existing betas of this base version | |
| LATEST_BETA=$(npm view "${PKG_NAME}" versions --json 2>/dev/null \ | |
| | node -p " | |
| const all = JSON.parse(require('fs').readFileSync(0,'utf8')); | |
| const betas = (Array.isArray(all) ? all : [all]) | |
| .filter(v => v.startsWith('${NEXT_BASE}-beta.')) | |
| .sort((a,b) => { | |
| const na = Number(a.split('-beta.')[1]); | |
| const nb = Number(b.split('-beta.')[1]); | |
| return na - nb; | |
| }); | |
| betas.length ? betas[betas.length-1] : '' | |
| " \ | |
| ) || true | |
| if [[ -n "$LATEST_BETA" && "$LATEST_BETA" == ${NEXT_BASE}-beta.* ]]; then | |
| # Existing beta found — increment from it | |
| echo "Found existing beta on npm: $LATEST_BETA — incrementing" | |
| pnpm version "$LATEST_BETA" --no-git-tag-version --allow-same-version | |
| pnpm version prerelease --preid=beta --no-git-tag-version | |
| else | |
| # No beta yet — start at beta.0 | |
| echo "No existing beta for ${NEXT_BASE} — creating ${NEXT_BASE}-beta.0" | |
| pnpm version "${NEXT_BASE}-beta.0" --no-git-tag-version --allow-same-version | |
| fi | |
| NEW_VERSION=$(node -p "require('./package.json').version") | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "package-name=$PKG_NAME" >> "$GITHUB_OUTPUT" | |
| # Print install instructions to the job logs AND the run summary so | |
| # they are visible both while the run is in progress and on the run | |
| # summary tab afterwards. | |
| { | |
| echo "### Pre-release version: \`$NEW_VERSION\`" | |
| echo "" | |
| echo "#### Install" | |
| echo "\`\`\`bash" | |
| echo "npm install -g ${PKG_NAME}@${NEW_VERSION}" | |
| echo "# or" | |
| echo "bun install -g ${PKG_NAME}@${NEW_VERSION}" | |
| echo "\`\`\`" | |
| } | tee -a "$GITHUB_STEP_SUMMARY" | |
| - name: Publish pre-release to npm | |
| if: inputs.type == 'pre-release' | |
| run: MCPC_RELEASE=1 pnpm publish --access public --tag beta --provenance --no-git-checks | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Create GitHub pre-release | |
| if: inputs.type == 'pre-release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.prerelease.outputs.version }} | |
| name: v${{ steps.prerelease.outputs.version }} | |
| prerelease: true | |
| generate_release_notes: true | |
| # Append install instructions to the auto-generated "What's Changed" | |
| # so the release page shows users how to install this version. | |
| append_body: true | |
| body: | | |
| ## Install | |
| ```bash | |
| npm install -g ${{ steps.prerelease.outputs.package-name }}@${{ steps.prerelease.outputs.version }} | |
| # or | |
| bun install -g ${{ steps.prerelease.outputs.package-name }}@${{ steps.prerelease.outputs.version }} | |
| ``` | |
| ${{ steps.install-size.outputs.report }} | |
| target_commitish: ${{ github.sha }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| # ── Release ──────────────────────────────────────────────────── | |
| - name: Bump release version | |
| if: inputs.type == 'release' | |
| id: release | |
| run: | | |
| CURRENT=$(node -p "require('./package.json').version") | |
| echo "current=$CURRENT" >> "$GITHUB_OUTPUT" | |
| pnpm version "${{ inputs.version }}" --no-git-tag-version | |
| NEW_VERSION=$(node -p "require('./package.json').version") | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "package-name=$PKG_NAME" >> "$GITHUB_OUTPUT" | |
| # Print install instructions to the job logs AND the run summary so | |
| # they are visible both while the run is in progress and on the run | |
| # summary tab afterwards. | |
| { | |
| echo "### Release version: \`$NEW_VERSION\`" | |
| echo "" | |
| echo "#### Install" | |
| echo "\`\`\`bash" | |
| echo "npm install -g ${PKG_NAME}@${NEW_VERSION}" | |
| echo "# or" | |
| echo "bun install -g ${PKG_NAME}@${NEW_VERSION}" | |
| echo "\`\`\`" | |
| } | tee -a "$GITHUB_STEP_SUMMARY" | |
| - name: Update CHANGELOG.md | |
| if: inputs.type == 'release' | |
| run: | | |
| NEW_VERSION="${{ steps.release.outputs.version }}" | |
| CURRENT="${{ steps.release.outputs.current }}" | |
| TODAY=$(date +%Y-%m-%d) | |
| # Replace [Unreleased] heading with new version, keep an empty [Unreleased] section | |
| sed -i "s/^## \[Unreleased\]/## [Unreleased]\n\n## [$NEW_VERSION] - $TODAY/" CHANGELOG.md | |
| # Update comparison links at the bottom | |
| if grep -q "^\[Unreleased\]:" CHANGELOG.md; then | |
| sed -i "s|\[Unreleased\]: \(.*\)/compare/v[0-9.]*\.\.\.HEAD|[Unreleased]: \1/compare/v$NEW_VERSION...HEAD\n[$NEW_VERSION]: \1/compare/v$CURRENT...v$NEW_VERSION|" CHANGELOG.md | |
| fi | |
| - name: Commit version bump and changelog | |
| if: inputs.type == 'release' | |
| run: | | |
| NEW_VERSION="${{ steps.release.outputs.version }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add package.json pnpm-lock.yaml CHANGELOG.md README.md docs/REFERENCE.md docs/images/related-work.svg | |
| git commit -m "v${NEW_VERSION}" | |
| git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}" | |
| git push origin main | |
| git push origin "v${NEW_VERSION}" | |
| - name: Publish release to npm | |
| if: inputs.type == 'release' | |
| run: MCPC_RELEASE=1 pnpm publish --access public --provenance --no-git-checks | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Create GitHub release | |
| if: inputs.type == 'release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.release.outputs.version }} | |
| name: v${{ steps.release.outputs.version }} | |
| generate_release_notes: true | |
| # Append install instructions to the auto-generated "What's Changed" | |
| # so the release page shows users how to install this version. | |
| append_body: true | |
| body: | | |
| ## Install | |
| ```bash | |
| npm install -g ${{ steps.release.outputs.package-name }}@${{ steps.release.outputs.version }} | |
| # or | |
| bun install -g ${{ steps.release.outputs.package-name }}@${{ steps.release.outputs.version }} | |
| ``` | |
| ${{ steps.install-size.outputs.report }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} | |
| # ── Homebrew tap ─────────────────────────────────────────────── | |
| - name: Homebrew formula update instructions | |
| if: inputs.type == 'release' | |
| # The formula bump in apify/homebrew-tap is started by hand for now, so | |
| # this step only prints the command — it deliberately does not dispatch | |
| # anything. To automate it later, run the printed command in this step | |
| # (with GH_TOKEN: secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN) and keep it | |
| # last and continue-on-error, so a Homebrew hiccup can never fail a | |
| # release that is already tagged, published to npm and announced. | |
| # | |
| # The dispatched workflow points Formula/mcpc.rb at the new npm tarball, | |
| # installs and `brew test`s it on Linux and macOS, then merges the bump. | |
| # It polls the registry itself, so it tolerates publish propagation. | |
| continue-on-error: true | |
| env: | |
| VERSION: ${{ steps.release.outputs.version }} | |
| run: | | |
| { | |
| echo "### Homebrew" | |
| echo "" | |
| echo "The formula bump is manual for now. To ship \`${VERSION}\` to \`brew\`, run:" | |
| echo "" | |
| echo '```bash' | |
| echo "gh workflow run update_formula.yaml --repo apify/homebrew-tap \\" | |
| echo " --field package=mcpc --field npm_package=@apify/mcpc --field version=${VERSION}" | |
| echo '```' | |
| } | tee -a "$GITHUB_STEP_SUMMARY" |