feat(Registration): redeemable release window with permissionless reclaim after expiry #392
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: PR | |
| on: | |
| pull_request: | |
| types: [opened, edited, synchronize, reopened, labeled, unlabeled, ready_for_review] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| title: | |
| name: PR Title | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check PR title | |
| id: title | |
| uses: amannn/action-semantic-pull-request@v5 | |
| continue-on-error: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| types: | | |
| feat | |
| fix | |
| docs | |
| style | |
| refactor | |
| test | |
| chore | |
| ci | |
| perf | |
| revert | |
| requireScope: false | |
| subjectPattern: ^[a-zA-Z][-a-zA-Z0-9 .,():+/]+$ | |
| - name: Process results | |
| id: run | |
| run: | | |
| if [ "${{ steps.title.outcome }}" == "success" ]; then | |
| echo "result=PR Title Valid" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "result=Invalid - Use type(scope): description (e.g. feat(registry): add protocol lookup)" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Update PR comment | |
| if: always() | |
| uses: actions/github-script@v7 | |
| env: | |
| SECTION: PR Title | |
| RESULT: ${{ steps.run.outputs.result }} | |
| with: | |
| script: | | |
| const marker = "<!-- ci-summary -->"; | |
| const detailsMarker = "<!-- details-section -->"; | |
| const section = process.env.SECTION; | |
| const result = process.env.RESULT || "PR Title Check Passed"; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.payload.pull_request.number; | |
| 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 detailsSection = ""; | |
| if (existing?.body) { | |
| const parts = existing.body.split(detailsMarker); | |
| const tableSection = parts[0] || ""; | |
| if (parts[1]) detailsSection = `\n${detailsMarker}${parts[1]}`; | |
| 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(); | |
| } | |
| } | |
| } | |
| } | |
| rows[section] = result; | |
| const order = ["4naly3er Analysis", "Slither Analysis", "Contract Tests", "Coverage", "Documentation", "Format & Lint", "Deploy Contracts", "PR Title", "Labels"]; | |
| 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`; | |
| } | |
| let body = `${marker}\n## CI Summary\n\n${table}${detailsSection}`; | |
| // GitHub issue/PR comments are capped at 65536 characters. Fall back | |
| // to the summary table only when the cumulative body would overflow. | |
| const MAX_BODY = 65000; | |
| if (body.length > MAX_BODY) { | |
| body = `${marker}\n## CI Summary\n\n${table}\n\n_Per-section details omitted: combined body exceeded the ${MAX_BODY}-char comment limit. Follow the **View Report** links above for each section's full output._`; | |
| } | |
| 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 title invalid | |
| if: steps.title.outcome == 'failure' | |
| run: exit 1 | |
| labels: | |
| name: Labels | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Auto-label | |
| uses: actions/labeler@v5 | |
| continue-on-error: true | |
| with: | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Validate labels | |
| id: run | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const pull_number = context.payload.pull_request.number; | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner, repo, pull_number | |
| }); | |
| const labels = pr.labels.map(l => l.name); | |
| const required = [ | |
| "dotns-sdk", "smartcontracts", "other", | |
| "scope: registration", "scope: resolver", "scope: store", "scope: pop", | |
| "type: docs", "type: test", "dependencies" | |
| ]; | |
| const matched = labels.filter(l => required.includes(l)); | |
| const allLabels = labels.join(", ") || "None"; | |
| if (matched.length > 0) { | |
| core.setOutput("result", `Passed - ${matched.join(", ")}`); | |
| core.setOutput("details", allLabels); | |
| } else { | |
| core.setOutput("result", "Failed - Requires one of: dotns-sdk, smartcontracts, other, a scope label, type: docs, type: test, or dependencies"); | |
| core.setOutput("details", allLabels); | |
| core.setFailed("Missing required label"); | |
| } | |
| - name: Update PR comment | |
| if: always() | |
| uses: actions/github-script@v7 | |
| env: | |
| SECTION: Labels | |
| RESULT: ${{ steps.run.outputs.result }} | |
| DETAILS: ${{ steps.run.outputs.details }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| script: | | |
| const marker = "<!-- ci-summary -->"; | |
| const detailsMarker = "<!-- details-section -->"; | |
| const section = process.env.SECTION; | |
| const result = process.env.RESULT || "Unknown"; | |
| const details = process.env.DETAILS; | |
| const runUrl = process.env.RUN_URL; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.payload.pull_request.number; | |
| 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]; | |
| } | |
| } | |
| rows[section] = result; | |
| if (result.startsWith("Failed")) { | |
| existingDetails[section] = `<details>\n<summary><strong>${section}</strong></summary>\n\nApplied: ${details}\n\nRequired: dotns-sdk, smartcontracts, other, a scope label, type: docs, type: test, or dependencies\n\n[View run](${runUrl})\n\n</details>`; | |
| } else if (details && details !== "None") { | |
| existingDetails[section] = `<details>\n<summary><strong>${section}</strong></summary>\n\n${details}\n\n</details>`; | |
| } else { | |
| delete existingDetails[section]; | |
| } | |
| const order = ["4naly3er Analysis", "Slither Analysis", "Contract Tests", "Coverage", "Documentation", "Format & Lint", "Deploy Contracts", "PR Title", "Labels"]; | |
| 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 = ["4naly3er Analysis", "Slither Analysis", "Contract Tests", "Coverage", "Documentation", "Format & Lint", "Deploy Contracts", "PR Title", "Labels"]; | |
| 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")}`; | |
| } | |
| // GitHub issue/PR comments are capped at 65536 characters. Fall back | |
| // to the summary table only when the cumulative body would overflow. | |
| const MAX_BODY = 65000; | |
| if (body.length > MAX_BODY) { | |
| body = `${marker}\n## CI Summary\n\n${table}\n\n_Per-section details omitted: combined body exceeded the ${MAX_BODY}-char comment limit. Follow the **View Report** links above for each section's full output._`; | |
| } | |
| 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 }); | |
| } |