Skip to content
Open
Show file tree
Hide file tree
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 Mar 2, 2026
1f578b0
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee Mar 2, 2026
7c053bc
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee Mar 2, 2026
93740f4
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee Mar 2, 2026
5c233d7
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee Mar 2, 2026
d2aeff7
Update .github/workflows/sync-issue-labels-add.yml
cheese-cakee Mar 2, 2026
f3271af
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee Mar 2, 2026
4e32a76
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee Mar 2, 2026
85c0f43
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee Mar 2, 2026
2016819
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee Mar 2, 2026
b7904b8
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee Mar 2, 2026
b001025
fix: wire workflow dispatch inputs for label sync
cheese-cakee Mar 2, 2026
f49eacd
fix: avoid github interpolation in add workflow run step
cheese-cakee Mar 2, 2026
5a46bac
fix: harden label sync workflow permissions and dry-run
cheese-cakee Mar 2, 2026
a563743
fix: address exploreriii and coderabbit workflow feedback
cheese-cakee Mar 4, 2026
edf1e76
fix: reduce issue parser complexity for codacy
cheese-cakee Mar 4, 2026
5a8c5dd
ci: allow flagged StepSecurity endpoint for test workflow
cheese-cakee Mar 4, 2026
4de321e
fix: preserve workflow dry-run false input
cheese-cakee Mar 4, 2026
3f93668
ci: Retrigger CI checks
cheese-cakee Mar 4, 2026
8117566
Update .github/workflows/sync-issue-labels-compute.yml
cheese-cakee Mar 5, 2026
383a1ee
Update .github/workflows/pr-check-test.yml
cheese-cakee Mar 5, 2026
be500a2
chore: sync changelog with upstream 0.2.1 release
cheese-cakee Mar 5, 2026
fe3b8b0
chore: add unreleased changelog entry for CI endpoint allowlist
cheese-cakee Mar 5, 2026
5664415
ci: allow additional StepSecurity mirror endpoint
cheese-cakee Mar 5, 2026
5101a2a
fix: checkout PR branch to find script file in compute workflow
cheese-cakee Mar 17, 2026
b277585
fix: address CodeRabbit security and reliability feedback
cheese-cakee Mar 22, 2026
4b1ddef
fix: inline script logic in compute workflow to avoid checkout on pul…
cheese-cakee Mar 22, 2026
21b1f4c
fix: use pull_request trigger with inlined script (no checkout needed)
cheese-cakee Mar 22, 2026
72e9688
fix: add actions:write permission for artifact upload and workflow di…
cheese-cakee Mar 22, 2026
1eef48d
fix: address Copilot review feedback
cheese-cakee Mar 22, 2026
f89cc25
fix: add concurrency group to compute workflow to prevent parallel runs
cheese-cakee Mar 22, 2026
64c9873
fix: address CodeRabbit critical issues
cheese-cakee Mar 22, 2026
69d07da
fix: remove secrets context from job-level if (not available there ei…
cheese-cakee Mar 22, 2026
2c7594c
test: use ubuntu-latest for fork e2e testing
cheese-cakee Mar 23, 2026
fd09465
fix: simplify add workflow condition and fix dry_run fallback
cheese-cakee Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/pr-check-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jobs:
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
allowed-endpoints: |
ziply.mm.fcix.net:443
mirror.servaxnet.com:443
mirror.fmt-2.serverforge.org:443

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
128 changes: 128 additions & 0 deletions .github/workflows/sync-issue-labels-add.yml
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 }}

- 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 }}

- 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 }}

208 changes: 208 additions & 0 deletions .github/workflows/sync-issue-labels-compute.yml
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' }}
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();
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);
}

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 };

- 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
with:
workflow: .github/workflows/sync-issue-labels-add.yml
repo: ${{ github.repository }}
ref: main
token: ${{ secrets.GH_ACCESS_TOKEN }}
inputs: >-
{
"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 }}"
}

5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Changelog
# Changelog

All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org).
Expand Down Expand Up @@ -54,6 +54,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- chore: update spam list #1988
- chore: Update `bot-advanced-check.yml`, `bot-gfi-assign-on-comment.yml`, `bot-intermediate-assignment.yml`, `bot-linked-issue-enforcer.yml`, `unassign-on-comment.yml`, `working-on-comment.yml` workflow runner configuration
- Fix build failing in `publish.yml`
- Allowed `mirror.servaxnet.com:443` in `pr-check-test.yml` Harden-Runner allowlist for stable Kind download network egress.
- Allowed `mirror.fmt-2.serverforge.org:443` in `pr-check-test.yml` Harden-Runner allowlist for stable Kind download network egress.
- Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716)



Expand Down
Loading