Publish PR pre-release #12
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
| # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json | |
| # | |
| # Workflow to manually publish pre-release packages from a pull request. | |
| # This is useful for testing changes before they are merged. | |
| name: Publish PR pre-release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to publish' | |
| required: true | |
| type: number | |
| publish_packages: | |
| description: 'Publish packages to JSR/npm' | |
| type: boolean | |
| default: true | |
| publish_docs: | |
| description: 'Publish docs preview to Cloudflare' | |
| type: boolean | |
| default: true | |
| jobs: | |
| # =========================================================================== | |
| # Get PR information | |
| # =========================================================================== | |
| get-pr-info: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| head_sha: ${{ steps.pr.outputs.head_sha }} | |
| head_repo: ${{ steps.pr.outputs.head_repo }} | |
| steps: | |
| - name: Get PR info | |
| id: pr | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: ${{ inputs.pr_number }} | |
| }); | |
| if (pr.data.state !== 'open') { | |
| core.setFailed(`PR #${{ inputs.pr_number }} is not open`); | |
| return; | |
| } | |
| core.setOutput('head_sha', pr.data.head.sha); | |
| core.setOutput('head_repo', pr.data.head.repo.full_name); | |
| # =========================================================================== | |
| # Determine version | |
| # =========================================================================== | |
| determine-version: | |
| if: inputs.publish_packages | |
| needs: [get-pr-info] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| short_version: ${{ steps.version.outputs.short_version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ needs.get-pr-info.outputs.head_repo }} | |
| ref: ${{ needs.get-pr-info.outputs.head_sha }} | |
| - uses: ./.github/actions/setup-mise | |
| - id: version | |
| uses: ./.github/actions/determine-version | |
| with: | |
| pr_number: ${{ inputs.pr_number }} | |
| head_sha: ${{ needs.get-pr-info.outputs.head_sha }} | |
| # =========================================================================== | |
| # Build packages | |
| # =========================================================================== | |
| build-packages: | |
| if: inputs.publish_packages | |
| needs: [get-pr-info, determine-version] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| packages_table: ${{ steps.packages-table.outputs.table }} | |
| packages_links: ${{ steps.packages-table.outputs.links }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ needs.get-pr-info.outputs.head_repo }} | |
| ref: ${{ needs.get-pr-info.outputs.head_sha }} | |
| - uses: ./.github/actions/setup-mise | |
| - uses: ./.github/actions/determine-version | |
| with: | |
| pr_number: ${{ inputs.pr_number }} | |
| head_sha: ${{ needs.get-pr-info.outputs.head_sha }} | |
| - run: sudo npm install -g npm@latest && npm --version | |
| - run: | | |
| pnpm pack --recursive --filter='!./examples/**' --config.enable-pre-post-scripts=false | |
| # Remove private packages and CLI package | |
| for pkg in fedify-*.tgz; do | |
| if tar -xOzf "$pkg" package/package.json | jq -e '.private == true' > /dev/null 2>&1; then | |
| echo "Removing private package: $pkg" | |
| rm "$pkg" | |
| fi | |
| done | |
| rm fedify-cli-*.tgz | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: npm-packages | |
| path: fedify-*.tgz | |
| - name: Generate packages table | |
| id: packages-table | |
| run: | | |
| set -e | |
| VERSION="${{ needs.determine-version.outputs.version }}" | |
| SHORT_VERSION="${{ needs.determine-version.outputs.short_version }}" | |
| { | |
| echo 'table<<EOFTABLE' | |
| deno run -A scripts/generate_packages_table.ts --format=table "$VERSION" "$SHORT_VERSION" | |
| echo 'EOFTABLE' | |
| } >> $GITHUB_OUTPUT | |
| { | |
| echo 'links<<EOFLINKS' | |
| deno run -A scripts/generate_packages_table.ts --format=links "$VERSION" "$SHORT_VERSION" | |
| echo 'EOFLINKS' | |
| } >> $GITHUB_OUTPUT | |
| # =========================================================================== | |
| # Publish to JSR | |
| # =========================================================================== | |
| publish-jsr: | |
| if: inputs.publish_packages | |
| needs: [get-pr-info, determine-version] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ needs.get-pr-info.outputs.head_repo }} | |
| ref: ${{ needs.get-pr-info.outputs.head_sha }} | |
| - uses: ./.github/actions/setup-mise | |
| - uses: ./.github/actions/determine-version | |
| with: | |
| pr_number: ${{ inputs.pr_number }} | |
| head_sha: ${{ needs.get-pr-info.outputs.head_sha }} | |
| # Don't know why, but the .gitignore list is not overridden by include list | |
| # in deno.json: | |
| - run: 'rm -f src/vocab/.gitignore || true' | |
| working-directory: ${{ github.workspace }}/packages/fedify/ | |
| - name: Publish to JSR | |
| run: | | |
| set -ex | |
| mise run codegen | |
| max_attempts=5 | |
| attempt=1 | |
| until deno publish --allow-dirty --allow-slow-types; do | |
| exit_code=$? | |
| if [[ $attempt -ge $max_attempts ]]; then | |
| echo "deno publish failed after $max_attempts attempts" | |
| exit $exit_code | |
| fi | |
| echo "deno publish failed (attempt $attempt/$max_attempts), retrying in 30 seconds..." | |
| sleep 30 | |
| ((attempt++)) | |
| done | |
| # =========================================================================== | |
| # Publish to npm (trigger build.yaml) | |
| # =========================================================================== | |
| # Trigger build.yaml via workflow_dispatch to publish to npm. | |
| # This is required because npm's trusted publishing (OIDC) validates | |
| # the directly triggered workflow, not reusable workflows called via | |
| # workflow_call. By triggering build.yaml directly, npm sees build.yaml | |
| # as the entry point and validates against it. | |
| publish-npm: | |
| if: inputs.publish_packages | |
| needs: [build-packages, publish-jsr] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write | |
| steps: | |
| - name: Trigger build.yaml workflow | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'build.yaml', | |
| ref: 'main', | |
| inputs: { | |
| run_id: '${{ github.run_id }}', | |
| tag: 'pr-${{ inputs.pr_number }}' | |
| } | |
| }); | |
| console.log('Triggered build.yaml workflow with run_id=${{ github.run_id }}, tag=pr-${{ inputs.pr_number }}'); | |
| - name: Wait for npm publish to complete | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Wait a bit for the workflow to start | |
| await new Promise(resolve => setTimeout(resolve, 10000)); | |
| // Poll for the triggered workflow run | |
| const maxAttempts = 30; | |
| const pollInterval = 20000; // 20 seconds | |
| for (let attempt = 1; attempt <= maxAttempts; attempt++) { | |
| const runs = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'build.yaml', | |
| event: 'workflow_dispatch', | |
| per_page: 5 | |
| }); | |
| // Find the run that was triggered by this workflow | |
| const matchingRun = runs.data.workflow_runs.find(run => { | |
| // Check if it's recent (within last 5 minutes) | |
| const runTime = new Date(run.created_at); | |
| const now = new Date(); | |
| const diffMinutes = (now - runTime) / 1000 / 60; | |
| return diffMinutes < 5; | |
| }); | |
| if (matchingRun) { | |
| console.log(`Found workflow run: ${matchingRun.html_url}`); | |
| if (matchingRun.status === 'completed') { | |
| if (matchingRun.conclusion === 'success') { | |
| console.log('npm publish completed successfully'); | |
| return; | |
| } else { | |
| core.setFailed(`npm publish failed with conclusion: ${matchingRun.conclusion}`); | |
| return; | |
| } | |
| } | |
| console.log(`Attempt ${attempt}/${maxAttempts}: Workflow status is ${matchingRun.status}, waiting...`); | |
| } else { | |
| console.log(`Attempt ${attempt}/${maxAttempts}: Workflow run not found yet, waiting...`); | |
| } | |
| if (attempt < maxAttempts) { | |
| await new Promise(resolve => setTimeout(resolve, pollInterval)); | |
| } | |
| } | |
| core.setFailed('Timed out waiting for npm publish workflow to complete'); | |
| # =========================================================================== | |
| # Publish docs preview | |
| # =========================================================================== | |
| publish-docs: | |
| if: inputs.publish_docs | |
| needs: [get-pr-info, determine-version] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| deployment_url: ${{ steps.wrangler.outputs.deployment-url }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ needs.get-pr-info.outputs.head_repo }} | |
| ref: ${{ needs.get-pr-info.outputs.head_sha }} | |
| - uses: ./.github/actions/setup-mise | |
| - run: | | |
| set -ex | |
| EXTRA_NAV_TEXT=Stable \ | |
| EXTRA_NAV_LINK="$STABLE_DOCS_URL" \ | |
| SITEMAP_HOSTNAME="$UNSTABLE_DOCS_URL" \ | |
| JSR_REF_VERSION=unstable \ | |
| pnpm run build | |
| env: | |
| PLAUSIBLE_DOMAIN: ${{ secrets.PLAUSIBLE_DOMAIN }} | |
| STABLE_DOCS_URL: ${{ vars.STABLE_DOCS_URL }} | |
| UNSTABLE_DOCS_URL: ${{ vars.UNSTABLE_DOCS_URL }} | |
| working-directory: ${{ github.workspace }}/docs/ | |
| - id: wrangler | |
| uses: cloudflare/wrangler-action@v3 | |
| with: | |
| apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| gitHubToken: ${{ github.token }} | |
| command: >- | |
| pages deploy .vitepress/dist | |
| --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} | |
| --branch=pr-${{ inputs.pr_number }} | |
| packageManager: pnpm | |
| workingDirectory: ${{ github.workspace }}/docs/ | |
| # =========================================================================== | |
| # Post comment on PR | |
| # =========================================================================== | |
| comment-on-pr: | |
| needs: [get-pr-info, determine-version, build-packages, publish-npm, publish-docs] | |
| if: always() && needs.get-pr-info.result == 'success' && (needs.publish-npm.result == 'success' || needs.publish-docs.result == 'success') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Delete old comment | |
| uses: thollander/actions-comment-pull-request@v3 | |
| with: | |
| pr-number: ${{ inputs.pr_number }} | |
| comment-tag: publish | |
| mode: delete | |
| - name: Post comment | |
| uses: thollander/actions-comment-pull-request@v3 | |
| with: | |
| pr-number: ${{ inputs.pr_number }} | |
| comment-tag: publish | |
| message: | | |
| Pre-release has been published for this pull request: | |
| ${{ needs.publish-npm.result == 'success' && format('## Packages | |
| {0} | |
| {1} | |
| ', needs.build-packages.outputs.packages_table, needs.build-packages.outputs.packages_links) || '' }}${{ needs.publish-docs.result == 'success' && format('## Documentation | |
| The docs for this pull request have been published: | |
| <{0}>', needs.publish-docs.outputs.deployment_url) || '' }} | |
| # cSpell: ignore npmjs thollander |