fix: increase timeout robustness (#93) #222
Workflow file for this run
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: | |
| push: | |
| tags: | |
| - 'v*' | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "packages/cli/**" | |
| - "packages/ui/**" | |
| - ".github/workflows/release.yml" | |
| permissions: | |
| actions: write | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| prerelease: ${{ steps.version.outputs.prerelease }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.2.6" | |
| - name: Install dependencies | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bun install --frozen-lockfile | |
| for pkg in packages/*; do | |
| [ -f "$pkg/package.json" ] || continue | |
| grep -q '"file:' "$pkg/package.json" || continue | |
| grep -oP '"[^"]+": "file:\K[^"]+' "$pkg/package.json" | while read -r dep; do | |
| name=$(jq -r .name "$pkg/$dep/package.json") | |
| scope_dir="$pkg/node_modules/$(dirname "$name")" | |
| mkdir -p "$scope_dir" | |
| ln -sfn "$(cd "$pkg/$dep" && pwd)" "$pkg/node_modules/$name" | |
| done | |
| done | |
| - name: Extract version | |
| id: version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| TAG="${GITHUB_REF_NAME}" | |
| else | |
| VERSION="0.0.0-pr.${{ github.event.pull_request.number }}" | |
| TAG="" | |
| fi | |
| PRERELEASE="false" | |
| if [[ "$VERSION" == *-* ]]; then | |
| PRERELEASE="true" | |
| fi | |
| echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" | |
| echo "tag=${TAG}" >> "${GITHUB_OUTPUT}" | |
| echo "prerelease=${PRERELEASE}" >> "${GITHUB_OUTPUT}" | |
| - name: Build CLI (capture output) | |
| id: build | |
| continue-on-error: true | |
| working-directory: packages/cli | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| bun run build 2>&1 | tee "$GITHUB_WORKSPACE/release-build-output.txt" | |
| - name: Upload build output | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-build-output | |
| path: release-build-output.txt | |
| retention-days: 7 | |
| - name: Prepare package | |
| if: steps.build.outcome == 'success' | |
| working-directory: packages/cli | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| rm -rf bin | |
| npm version "${{ steps.version.outputs.version }}" --no-git-tag-version --allow-same-version | |
| jq 'del(.private)' package.json > package.json.tmp | |
| mv package.json.tmp package.json | |
| - name: Pack | |
| if: steps.build.outcome == 'success' | |
| working-directory: packages/cli | |
| run: npm pack | |
| - name: Upload CLI artifact | |
| if: steps.build.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cli-release-${{ steps.version.outputs.version }} | |
| path: packages/cli/*.tgz | |
| retention-days: 7 | |
| - name: Process results | |
| id: result | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ steps.build.outcome }}" == "success" ]]; then | |
| echo "status=Passed" >> "${GITHUB_OUTPUT}" | |
| echo "details=Build succeeded" >> "${GITHUB_OUTPUT}" | |
| else | |
| echo "status=Failed" >> "${GITHUB_OUTPUT}" | |
| echo "details=Package build failed" >> "${GITHUB_OUTPUT}" | |
| fi | |
| - name: Update PR comment | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| env: | |
| SECTION: Release | |
| STATUS: ${{ steps.result.outputs.status }} | |
| DETAILS: ${{ steps.result.outputs.details }} | |
| VERSION: ${{ steps.version.outputs.version }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| RUN_ID: ${{ github.run_id }} | |
| REPO: ${{ github.repository }} | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const marker = "<!-- ci-summary -->"; | |
| const detailsMarker = "<!-- details-section -->"; | |
| const section = process.env.SECTION; | |
| const status = process.env.STATUS; | |
| const details = process.env.DETAILS; | |
| const version = process.env.VERSION; | |
| const runUrl = process.env.RUN_URL; | |
| const runId = process.env.RUN_ID; | |
| const repoFull = process.env.REPO; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.payload.pull_request.number; | |
| let output = ""; | |
| try { | |
| output = fs.readFileSync("release-build-output.txt", "utf8"); | |
| } catch (_) { | |
| output = "(release-build-output.txt not found)"; | |
| } | |
| const MAX_CHARS = 60000; | |
| if (output.length > MAX_CHARS) { | |
| output = ["(truncated; showing last " + MAX_CHARS + " chars)", "", output.slice(-MAX_CHARS)].join("\n"); | |
| } | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, repo, issue_number, per_page: 100, | |
| }); | |
| const existing = comments.find(c => | |
| c.user?.login === "github-actions[bot]" && c.body?.includes(marker) | |
| ); | |
| let rows = {}; | |
| let existingDetails = {}; | |
| if (existing?.body) { | |
| const parts = existing.body.split(detailsMarker); | |
| const tableSection = parts[0] || ""; | |
| const lines = tableSection.split("\n"); | |
| for (const line of lines) { | |
| const match = line.match(/^\| ([^|]+) \| ([^|]+) \|$/); | |
| if (match) { | |
| const name = match[1].trim(); | |
| if (name && name !== "Check" && !name.startsWith(":")) { | |
| rows[name] = match[2].trim(); | |
| } | |
| } | |
| } | |
| const detailsRegex = /<details>\s*<summary><strong>([^<]+)<\/strong>.*?<\/summary>([\s\S]*?)<\/details>/g; | |
| let detailMatch; | |
| while ((detailMatch = detailsRegex.exec(existing.body)) !== null) { | |
| existingDetails[detailMatch[1].trim()] = detailMatch[0]; | |
| } | |
| } | |
| const resultText = status === "Passed" ? "Passed" : "Failed"; | |
| rows[section] = `[${resultText}](${runUrl})`; | |
| if (status === "Passed") { | |
| const installInstructions = [ | |
| "**Test this PR**", | |
| "", | |
| "Download artifact (GitHub CLI required):", | |
| "```bash", | |
| `gh run download ${runId} -n cli-release-${version} -R ${repoFull}`, | |
| "```", | |
| "", | |
| "Install globally:", | |
| "```bash", | |
| `npm install -g ./parity-dotns-cli-${version}.tgz`, | |
| "```", | |
| "", | |
| "Verify:", | |
| "```bash", | |
| "dotns --help", | |
| "```" | |
| ].join("\n"); | |
| existingDetails[section] = [ | |
| `<details>`, | |
| `<summary><strong>${section}</strong> - ${resultText}</summary>`, | |
| "", | |
| installInstructions, | |
| "", | |
| "</details>", | |
| ].join("\n"); | |
| } else { | |
| existingDetails[section] = [ | |
| `<details>`, | |
| `<summary><strong>${section}</strong> - ${resultText}</summary>`, | |
| "", | |
| details || "Failed", | |
| "", | |
| `[View run](${runUrl})`, | |
| "", | |
| "```text", | |
| output, | |
| "```", | |
| "</details>", | |
| ].join("\n"); | |
| } | |
| const order = ["Lint", "Lint (UI)", "Format", "Format (UI)", "Typecheck", "Typecheck (UI)", "Build", "Build (UI)", "Release", "Deploy UI", "Deploy Example", "Benchmark UI", "PR Title", "Labels", "Test"]; | |
| const sortedKeys = Object.keys(rows).sort((a, b) => { | |
| const ai = order.indexOf(a), bi = order.indexOf(b); | |
| return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); | |
| }); | |
| let table = `| Check | Result |\n|:------|:-------|\n`; | |
| for (const key of sortedKeys) { | |
| table += `| ${key} | ${rows[key]} |\n`; | |
| } | |
| const detailsOrder = ["Lint", "Lint (UI)", "Format", "Format (UI)", "Typecheck", "Typecheck (UI)", "Build", "Build (UI)", "Release", "Deploy UI", "Deploy Example", "Benchmark UI", "PR Title", "Labels", "Test"]; | |
| const sortedDetails = Object.keys(existingDetails).sort((a, b) => { | |
| const ai = detailsOrder.indexOf(a), bi = detailsOrder.indexOf(b); | |
| return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); | |
| }); | |
| let body = `${marker}\n## CI Summary\n\n${table}`; | |
| if (sortedDetails.length > 0) { | |
| body += `\n${detailsMarker}\n\n---\n\n${sortedDetails.map(k => existingDetails[k]).join("\n\n")}`; | |
| } | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | |
| } | |
| - name: Fail if build failed | |
| if: steps.build.outcome == 'failure' | |
| run: exit 1 | |
| build-ui: | |
| name: Build UI | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.2.6" | |
| - name: Install dependencies | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bun install --frozen-lockfile | |
| for pkg in packages/*; do | |
| [ -f "$pkg/package.json" ] || continue | |
| grep -q '"file:' "$pkg/package.json" || continue | |
| grep -oP '"[^"]+": "file:\K[^"]+' "$pkg/package.json" | while read -r dep; do | |
| name=$(jq -r .name "$pkg/$dep/package.json") | |
| scope_dir="$pkg/node_modules/$(dirname "$name")" | |
| mkdir -p "$scope_dir" | |
| ln -sfn "$(cd "$pkg/$dep" && pwd)" "$pkg/node_modules/$name" | |
| done | |
| done | |
| - name: Extract version | |
| id: version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| else | |
| VERSION="0.0.0-pr.${{ github.event.pull_request.number }}" | |
| fi | |
| echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" | |
| - name: Build UI | |
| working-directory: packages/ui | |
| run: bun run build | |
| - name: Create UI tarball | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ steps.version.outputs.version }}" | |
| tar -czf "dotns-ui-${VERSION}.tar.gz" -C packages/ui/dist . | |
| - name: Upload UI tarball | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ui-release-tarball | |
| path: dotns-ui-*.tar.gz | |
| retention-days: 7 | |
| - name: Upload UI site artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ui-site | |
| path: packages/ui/dist/ | |
| retention-days: 7 | |
| release: | |
| name: Create Release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: [build, build-ui] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download CLI artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: cli-release-${{ needs.build.outputs.version }} | |
| path: artifacts | |
| - name: Download UI artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ui-release-tarball | |
| path: artifacts | |
| - name: Create release body | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ needs.build.outputs.version }}" | |
| TAG="${{ needs.build.outputs.tag }}" | |
| REPO="${{ github.repository }}" | |
| cat > release-body.md <<EOF | |
| ## Installation | |
| ### CLI | |
| Install from npm: | |
| \`\`\`bash | |
| npm install -g @parity/dotns-cli | |
| \`\`\` | |
| Or with other package managers: | |
| | Package Manager | Command | | |
| |:----------------|:--------| | |
| | npm | \`npm install -g @parity/dotns-cli\` | | |
| | yarn | \`yarn global add @parity/dotns-cli\` | | |
| | bun | \`bun add -g @parity/dotns-cli\` | | |
| | pnpm | \`pnpm add -g @parity/dotns-cli\` | | |
| Verify: | |
| \`\`\`bash | |
| dotns --help | |
| \`\`\` | |
| ### UI | |
| Download: | |
| \`\`\`bash | |
| gh release download ${TAG} -p "dotns-ui-*.tar.gz" -R ${REPO} | |
| \`\`\` | |
| Extract: | |
| \`\`\`bash | |
| mkdir -p dotns-ui && tar -xzf dotns-ui-${VERSION}.tar.gz -C dotns-ui | |
| \`\`\` | |
| EOF | |
| - name: Create release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| artifacts/*.tgz | |
| artifacts/dotns-ui-*.tar.gz | |
| body_path: release-body.md | |
| generate_release_notes: true | |
| prerelease: ${{ needs.build.outputs.prerelease == 'true' }} | |
| - name: Trigger NPM Release | |
| if: needs.build.outputs.prerelease != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh workflow run npm-release.yml \ | |
| --ref main \ | |
| -f "tag=${{ needs.build.outputs.tag }}" \ | |
| -R "${{ github.repository }}" | |
| deploy-production: | |
| name: Deploy Production | |
| if: github.event_name == 'push' && !contains(github.ref_name, '-') | |
| needs: [build-ui] | |
| uses: ./.github/workflows/deploy.yml | |
| with: | |
| basename: dotns | |
| mode: production | |
| artifact-name: ui-site | |
| cli-source: release | |
| secrets: | |
| dotns-mnemonic: ${{ secrets.DOTNS_MNEMONIC }} | |
| bulletin-mnemonic: ${{ secrets.DOTNS_MNEMONIC_DEPLOY_UI }} |