-
Notifications
You must be signed in to change notification settings - Fork 235
feat: sync linked issue labels to pull requests #2015
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cheese-cakee
wants to merge
35
commits into
hiero-ledger:main
Choose a base branch
from
cheese-cakee:feat/sync-issue-labels
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+344
−1
Open
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
cc38b76
feat: sync linked issue labels to pull requests
cheese-cakee 1f578b0
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee 7c053bc
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee 93740f4
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee 5c233d7
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee d2aeff7
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee f3271af
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee 4e32a76
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee 85c0f43
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee 2016819
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee b7904b8
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee b001025
fix: wire workflow dispatch inputs for label sync
cheese-cakee f49eacd
fix: avoid github interpolation in add workflow run step
cheese-cakee 5a46bac
fix: harden label sync workflow permissions and dry-run
cheese-cakee a563743
fix: address exploreriii and coderabbit workflow feedback
cheese-cakee edf1e76
fix: reduce issue parser complexity for codacy
cheese-cakee 5a8c5dd
ci: allow flagged StepSecurity endpoint for test workflow
cheese-cakee 4de321e
fix: preserve workflow dry-run false input
cheese-cakee 3f93668
ci: Retrigger CI checks
cheese-cakee 8117566
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee 383a1ee
Update .github/workflows/pr-check-test.yml
cheese-cakee be500a2
chore: sync changelog with upstream 0.2.1 release
cheese-cakee fe3b8b0
chore: add unreleased changelog entry for CI endpoint allowlist
cheese-cakee 5664415
ci: allow additional StepSecurity mirror endpoint
cheese-cakee 5101a2a
fix: checkout PR branch to find script file in compute workflow
cheese-cakee b277585
fix: address CodeRabbit security and reliability feedback
cheese-cakee 4b1ddef
fix: inline script logic in compute workflow to avoid checkout on pul…
cheese-cakee 21b1f4c
fix: use pull_request trigger with inlined script (no checkout needed)
cheese-cakee 72e9688
fix: add actions:write permission for artifact upload and workflow di…
cheese-cakee 1eef48d
fix: address Copilot review feedback
cheese-cakee f89cc25
fix: add concurrency group to compute workflow to prevent parallel runs
cheese-cakee 64c9873
fix: address CodeRabbit critical issues
cheese-cakee 69d07da
fix: remove secrets context from job-level if (not available there ei…
cheese-cakee 2c7594c
test: use ubuntu-latest for fork e2e testing
cheese-cakee fd09465
fix: simplify add workflow condition and fix dry_run fallback
cheese-cakee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| name: Add Linked Issue Labels to PR | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| upstream_run_id: | ||
| description: "Upstream compute workflow run ID" | ||
| required: true | ||
| type: string | ||
| pr_number: | ||
| description: "Pull request number" | ||
| required: true | ||
| type: string | ||
| dry_run: | ||
| description: "Dry run flag" | ||
| required: false | ||
| type: string | ||
| default: "true" | ||
| is_fork_pr: | ||
| description: "Fork PR flag" | ||
| required: false | ||
| type: string | ||
| default: "false" | ||
| defaults: | ||
| run: | ||
| shell: bash | ||
| permissions: | ||
| actions: read | ||
| issues: write | ||
|
|
||
| jobs: | ||
| add-labels: | ||
| concurrency: | ||
| group: sync-issue-labels-pr-${{ github.event.inputs.pr_number }} | ||
| cancel-in-progress: true | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Harden the runner | ||
| uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: Download labels artifact | ||
| id: download | ||
| continue-on-error: true | ||
| uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 | ||
| with: | ||
| name: pr-labels-${{ github.event.inputs.pr_number }} | ||
| path: artifacts | ||
| run-id: ${{ github.event.inputs.upstream_run_id }} | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - name: Read labels payload | ||
| id: read | ||
| env: | ||
| INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} | ||
| INPUT_IS_FORK_PR: ${{ github.event.inputs.is_fork_pr }} | ||
| INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }} | ||
| run: | | ||
| labels_file="artifacts/labels.json" | ||
| if [ ! -f "$labels_file" ]; then | ||
| echo "::error::Labels artifact not found. Cross-workflow handoff is broken." | ||
| echo "labels=[]" >> "$GITHUB_OUTPUT" | ||
| echo "labels_count=0" >> "$GITHUB_OUTPUT" | ||
| echo "labels_multiline=" >> "$GITHUB_OUTPUT" | ||
| echo "pr_number=$INPUT_PR_NUMBER" >> "$GITHUB_OUTPUT" | ||
| echo "is_fork_pr=$INPUT_IS_FORK_PR" >> "$GITHUB_OUTPUT" | ||
| echo "dry_run=$INPUT_DRY_RUN" >> "$GITHUB_OUTPUT" | ||
| echo "source_event=workflow_dispatch" >> "$GITHUB_OUTPUT" | ||
| exit 1 | ||
| fi | ||
| labels=$(jq -c '.labels // []' "$labels_file") | ||
| pr_number=$(jq -r '.pr_number // 0' "$labels_file") | ||
| is_fork_pr=$(jq -r '.is_fork_pr // false' "$labels_file") | ||
| dry_run=$(jq -r '.dry_run // "true"' "$labels_file") | ||
| source_event=$(jq -r '.source_event // ""' "$labels_file") | ||
| labels_multiline=$(jq -r '.labels // [] | .[]' "$labels_file") | ||
| labels_count=$(echo "$labels" | jq 'length') | ||
| echo "labels=$labels" >> "$GITHUB_OUTPUT" | ||
| echo "labels_count=$labels_count" >> "$GITHUB_OUTPUT" | ||
| { | ||
| echo "labels_multiline<<EOF" | ||
| echo "$labels_multiline" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" | ||
| echo "is_fork_pr=$is_fork_pr" >> "$GITHUB_OUTPUT" | ||
| echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT" | ||
| echo "source_event=$source_event" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Validate labels payload | ||
| id: validate | ||
| run: | | ||
| if [ "$PR_NUMBER" = "0" ] || [ -z "$LABELS" ]; then | ||
| echo "Invalid payload: pr_number=$PR_NUMBER or labels empty. Skipping label addition." | ||
| echo "valid_payload=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "valid_payload=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| env: | ||
| PR_NUMBER: ${{ steps.read.outputs.pr_number }} | ||
| LABELS: ${{ steps.read.outputs.labels }} | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - name: Determine if labels should be applied | ||
| id: should_apply | ||
| run: | | ||
| if [ "${{ steps.read.outputs.is_fork_pr }}" = "true" ]; then | ||
| echo "apply=false" >> "$GITHUB_OUTPUT" | ||
| echo "reason=fork PR" >> "$GITHUB_OUTPUT" | ||
| elif [ "${{ steps.validate.outputs.valid_payload }}" != "true" ]; then | ||
| echo "apply=false" >> "$GITHUB_OUTPUT" | ||
| echo "reason=invalid payload" >> "$GITHUB_OUTPUT" | ||
| elif [ "${{ steps.read.outputs.source_event }}" = "workflow_dispatch" ] && [ "${{ steps.read.outputs.dry_run }}" = "true" ]; then | ||
| echo "apply=false" >> "$GITHUB_OUTPUT" | ||
| echo "reason=dry run" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "apply=true" >> "$GITHUB_OUTPUT" | ||
| echo "reason=" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Add labels to PR | ||
| if: ${{ steps.should_apply.outputs.apply == 'true' }} | ||
| uses: actions-ecosystem/action-add-labels@1a9c3715c0037e96b97bb38cb4c4b56a1f1d4871 # main | ||
| with: | ||
| github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| labels: ${{ steps.read.outputs.labels_multiline }} | ||
| number: ${{ steps.read.outputs.pr_number }} | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| name: Compute Linked Issue Labels | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, edited, reopened, synchronize, ready_for_review] | ||
| workflow_dispatch: | ||
| inputs: | ||
| pr_number: | ||
| description: "PR number to sync labels for" | ||
| required: true | ||
| type: number | ||
| dry-run-enabled: | ||
| description: "Dry run (log only, do not apply labels)" | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
|
|
||
| permissions: | ||
| actions: write | ||
| pull-requests: read | ||
| issues: read | ||
| contents: read | ||
|
|
||
| jobs: | ||
| compute-labels: | ||
| concurrency: | ||
| group: sync-issue-labels-compute-pr-${{ github.event.pull_request.number || github.event.inputs.pr_number }} | ||
| cancel-in-progress: true | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| pr_number: ${{ steps.compute.outputs.pr_number }} | ||
| dry_run: ${{ steps.compute.outputs.dry_run }} | ||
| is_fork_pr: ${{ steps.compute.outputs.is_fork_pr }} | ||
| steps: | ||
| - name: Harden the runner | ||
| uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: Compute linked issue labels | ||
| id: compute | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} | ||
| DRY_RUN: 'true' | ||
| REQUESTED_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && format('{0}', github.event.inputs['dry-run-enabled']) || 'true' }} | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork || 'false' }} | ||
| MAX_LINKED_ISSUES: '20' | ||
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | ||
| with: | ||
| result-encoding: json | ||
| script: | | ||
| const MAX_LINKED_ISSUES = Number(process.env.MAX_LINKED_ISSUES || "20"); | ||
|
|
||
| function extractLabels(labelData) { | ||
| const result = []; | ||
| for (const item of labelData) { | ||
| const name = typeof item === "string" ? item : item && item.name; | ||
| if (name && name.trim()) result.push(name.trim()); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| function extractLinkedIssueNumbers(prBody, owner, repo) { | ||
| const numbers = new Set(); | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const closingRefRegex = /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+(?:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+))?#(\d+)\b/gi; | ||
| const lines = String(prBody || "").split(/\r?\n/); | ||
| for (const line of lines) { | ||
| let m; | ||
| while ((m = closingRefRegex.exec(line)) !== null) { | ||
| const refOwner = (m[1] || "").toLowerCase(); | ||
| const refRepo = (m[2] || "").toLowerCase(); | ||
| if (refOwner && refRepo && (refOwner !== owner.toLowerCase() || refRepo !== repo.toLowerCase())) continue; | ||
| numbers.add(Number(m[3])); | ||
| } | ||
| } | ||
| const all = Array.from(numbers); | ||
| if (all.length > MAX_LINKED_ISSUES) { | ||
| console.log(`[sync] Limiting linked issue refs from ${all.length} to ${MAX_LINKED_ISSUES}.`); | ||
| } | ||
| return all.slice(0, MAX_LINKED_ISSUES); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| const prNumber = Number(process.env.PR_NUMBER); | ||
| if (!prNumber) { | ||
| core.setOutput('has_labels', 'false'); | ||
| core.setOutput('labels', '[]'); | ||
| core.setOutput('pr_number', ''); | ||
| core.setOutput('dry_run', 'true'); | ||
| core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); | ||
| core.setOutput('source_event', context.eventName); | ||
| return; | ||
| } | ||
|
|
||
| const { data: prData } = await github.rest.pulls.get({ | ||
| owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber | ||
| }); | ||
|
|
||
| const prAuthor = (prData.user && prData.user.login) || ""; | ||
| if (/\[bot\]$/i.test(prAuthor) || /dependabot/i.test(prAuthor)) { | ||
| console.log(`[sync] Skipping bot-authored PR from ${prAuthor}.`); | ||
| core.setOutput('has_labels', 'false'); | ||
| core.setOutput('labels', '[]'); | ||
| core.setOutput('pr_number', String(prNumber)); | ||
| core.setOutput('dry_run', 'true'); | ||
| core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); | ||
| core.setOutput('source_event', context.eventName); | ||
| return; | ||
| } | ||
|
|
||
| const linkedIssues = extractLinkedIssueNumbers(prData.body || "", context.repo.owner, context.repo.repo); | ||
| if (!linkedIssues.length) { | ||
| console.log("[sync] No linked issue references found in PR body."); | ||
| core.setOutput('has_labels', 'false'); | ||
| core.setOutput('labels', '[]'); | ||
| core.setOutput('pr_number', String(prNumber)); | ||
| core.setOutput('dry_run', 'true'); | ||
| core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); | ||
| core.setOutput('source_event', context.eventName); | ||
| return; | ||
| } | ||
|
|
||
| console.log(`[sync] Linked issues: ${linkedIssues.map(n => '#' + n).join(', ')}`); | ||
|
|
||
| const allLabels = []; | ||
| for (const num of linkedIssues) { | ||
| try { | ||
| const { data } = await github.rest.issues.get({ | ||
| owner: context.repo.owner, repo: context.repo.repo, issue_number: num | ||
| }); | ||
| if (data.pull_request) { console.log(`[sync] Skipping #${num}: is a PR reference.`); continue; } | ||
| const labels = extractLabels(data.labels || []); | ||
| console.log(`[sync] Issue #${num} labels: ${labels.length ? labels.join(', ') : '(none)'}`); | ||
| allLabels.push(...labels); | ||
| } catch (err) { | ||
| if (err && err.status === 404) { console.log(`[sync] Issue #${num} not found. Skipping.`); continue; } | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| const existing = extractLabels(prData.labels || []); | ||
| const existingSet = new Set(existing); | ||
| const deduped = Array.from(new Set(allLabels)); | ||
| const toAdd = deduped.filter(l => !existingSet.has(l)); | ||
|
|
||
| console.log(`[sync] Existing: ${existing.length ? existing.join(', ') : '(none)'}`); | ||
| console.log(`[sync] To add: ${toAdd.length ? toAdd.join(', ') : '(none)'}`); | ||
|
|
||
| const labels = toAdd; | ||
| const hasLabels = labels.length > 0; | ||
| core.setOutput('has_labels', String(hasLabels)); | ||
| core.setOutput('labels', JSON.stringify(labels)); | ||
| core.setOutput('pr_number', String(prNumber)); | ||
| core.setOutput('dry_run', String(process.env.REQUESTED_DRY_RUN || 'true')); | ||
| core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); | ||
| core.setOutput('source_event', context.eventName); | ||
| return { has_labels: hasLabels, labels, pr_number: String(prNumber), dry_run: process.env.REQUESTED_DRY_RUN, is_fork_pr: process.env.IS_FORK_PR, source_event: context.eventName }; | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - name: Write labels artifact payload | ||
| env: | ||
| LABELS_JSON: ${{ steps.compute.outputs.labels }} | ||
| PR_NUMBER: ${{ steps.compute.outputs.pr_number }} | ||
| IS_FORK_PR: ${{ steps.compute.outputs.is_fork_pr }} | ||
| DRY_RUN: ${{ steps.compute.outputs.dry_run }} | ||
| SOURCE_EVENT: ${{ steps.compute.outputs.source_event }} | ||
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
| const parsed = JSON.parse(process.env.LABELS_JSON || '[]'); | ||
| const payload = { | ||
| pr_number: Number(process.env.PR_NUMBER || 0), | ||
| labels: Array.isArray(parsed) ? parsed : [], | ||
| is_fork_pr: /^true$/i.test(process.env.IS_FORK_PR || ''), | ||
| dry_run: /^true$/i.test(process.env.DRY_RUN || ''), | ||
| source_event: process.env.SOURCE_EVENT || '', | ||
| }; | ||
| fs.writeFileSync('labels.json', JSON.stringify(payload)); | ||
| console.log(`Wrote labels artifact payload for PR #${payload.pr_number}: ${payload.labels.length} labels`); | ||
|
|
||
| - name: Upload labels artifact | ||
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | ||
| with: | ||
| name: pr-labels-${{ steps.compute.outputs.pr_number }} | ||
| path: labels.json | ||
| retention-days: 1 | ||
|
|
||
| dispatch-add: | ||
| needs: compute-labels | ||
| if: ${{ needs.compute-labels.outputs.is_fork_pr != 'true' }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Trigger add workflow | ||
| continue-on-error: true | ||
| uses: step-security/workflow-dispatch@acca1a315af3bf7f33dd116d3cb405cb83f5cbdc # v1.2.8 | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| with: | ||
| workflow: .github/workflows/sync-issue-labels-add.yml | ||
| repo: ${{ github.repository }} | ||
| ref: main | ||
| token: ${{ secrets.GH_ACCESS_TOKEN }} | ||
cheese-cakee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| inputs: >- | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| "upstream_run_id":"${{ github.run_id }}", | ||
| "pr_number":"${{ needs.compute-labels.outputs.pr_number }}", | ||
| "dry_run":"${{ needs.compute-labels.outputs.dry_run }}", | ||
| "is_fork_pr":"${{ needs.compute-labels.outputs.is_fork_pr }}" | ||
| } | ||
|
|
||
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.