Files
ProxmoxVE/.github/workflows/close_issue_in_dev.yaml
T
CanbiZ (MickLesk) 1c1295a9fd Point to DevScripts, the renamed ProxmoxVED (#17694)
Contribution docs, the PR template and the workflows that talk to the
testing repository use the new name. Migration PRs are matched by either
name, and three license headers pointed at contributor forks.
2026-10-05 11:03:02 +02:00

261 lines
11 KiB
YAML
Generated

name: Close Matching Issue on PR Merge
on:
pull_request:
types:
- closed
permissions:
contents: read
jobs:
close_issue:
if: github.event.pull_request.merged == true && github.repository == 'community-scripts/ProxmoxVE'
runs-on: self-hosted
env:
DEV_REPO: community-scripts/DevScripts
steps:
- name: Checkout target repo (merge commit)
uses: actions/checkout@v7
with:
repository: community-scripts/ProxmoxVE
ref: ${{ github.event.pull_request.merge_commit_sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Collect script slugs from the merged PR
id: get_slugs
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
slugs=""
while IFS= read -r path; do
[ -n "$path" ] || continue
[ -f "$path" ] || continue
case "$path" in
frontend/public/json/*.json)
s=$(jq -r '.slug // empty' "$path" 2>/dev/null || true)
;;
install/*.sh)
s=$(basename "$path" .sh)
s="${s%-install}"
;;
ct/*.sh|tools/*.sh|turnkey/*.sh|vm/*.sh)
s=$(basename "$path" .sh)
;;
*)
s=""
;;
esac
if [ -n "$s" ]; then
slugs="$slugs $s"
fi
done < <(gh pr view "$PR_NUMBER" --repo community-scripts/ProxmoxVE --json files --jq '.files[].path')
slugs=$(printf '%s\n' $slugs | sed '/^$/d' | sort -u | tr '\n' ' ' | xargs || true)
printf '%s' "$slugs" > pocketbase_slugs.txt
echo "count=$(printf '%s' "$slugs" | wc -w | tr -d '[:space:]')" >> "$GITHUB_OUTPUT"
echo "Slugs in this PR: ${slugs:-<none>}"
- name: Resolve the matching DevScripts issue
id: find_issue
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# move-to-main-repo.yaml stamps the source issue into the PR body it
# creates, so migration PRs carry an exact back-reference. Guessing from
# titles or file names is never needed for them.
matched=$(printf '%s' "$PR_BODY" \
| grep -oE 'community-scripts/(ProxmoxVED|DevScripts)#[0-9]+' \
| sed 's/.*#//' || true)
if [ -n "$matched" ]; then
echo "Back-reference in PR body -> issue(s): $(printf '%s' "$matched" | tr '\n' ' ')"
else
slugs=$(cat pocketbase_slugs.txt)
if [ -z "$slugs" ]; then
echo "No back-reference and no script slugs — nothing to close."
else
# Hand-made PR: fall back to an exact slug match, and only against
# issues a maintainer already put into migration. A substring match
# here would close unrelated issues that merely mention the app.
issues=$(gh issue list --repo "$DEV_REPO" --state open --limit 500 \
--json number,title,body,labels \
| jq -c '[.[] | select([.labels[].name]
| any(. == "Started Migration To ProxmoxVE" or . == "Migration To ProxmoxVE"))]')
echo "Migration candidates in $DEV_REPO: $(printf '%s' "$issues" | jq 'length')"
for slug in $slugs; do
while IFS= read -r row; do
[ -n "$row" ] || continue
num=$(printf '%s' "$row" | jq -r '.number')
# Same slug derivation the DevScripts workflows use, so both
# sides agree on what "alpine-cinny" means.
name=$(printf '%s' "$row" | jq -r '.body // ""' \
| sed -n '/^###[[:space:]]*Name of the Script/,/^###/p' \
| sed '1d;/^###/d;/^[[:space:]]*$/d' | head -n1)
[ -n "$name" ] || name=$(printf '%s' "$row" | jq -r '.title')
issue_slug=$(printf '%s' "$name" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | sed 's/[[:space:]]\+/-/g')
if [ "$issue_slug" = "$slug" ]; then
echo "Exact slug match: $slug -> #$num"
matched="$matched $num"
fi
done < <(printf '%s' "$issues" | jq -c '.[]')
done
fi
fi
matched=$(printf '%s\n' $matched | sed '/^$/d' | sort -un | tr '\n' ' ' | xargs || true)
count=$(printf '%s' "$matched" | wc -w | tr -d '[:space:]')
if [ "$count" -gt 3 ]; then
echo "::error::Refusing to close $count issues for a single PR ($matched) — matching is wrong."
exit 1
fi
echo "issue_numbers=$matched" >> "$GITHUB_OUTPUT"
echo "Closing: ${matched:-<none>}"
- name: Comment on and close matching DevScripts issues
if: steps.find_issue.outputs.issue_numbers != ''
shell: bash
env:
GH_TOKEN: ${{ secrets.GH_CROSS_REPO_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ISSUE_NUMBERS: ${{ steps.find_issue.outputs.issue_numbers }}
run: |
for issue_number in $ISSUE_NUMBERS; do
state=$(gh issue view "$issue_number" --repo "$DEV_REPO" --json state --jq '.state' 2>/dev/null || echo "MISSING")
if [ "$state" != "OPEN" ]; then
echo "Skipping #$issue_number (state: $state)"
continue
fi
echo "Closing $DEV_REPO#$issue_number"
gh issue comment "$issue_number" --repo "$DEV_REPO" \
--body "Merged with #${PR_NUMBER} in ProxmoxVE"
gh issue close "$issue_number" --repo "$DEV_REPO"
done
- name: Set is_dev to false in PocketBase
if: steps.get_slugs.outputs.count != '0'
env:
POCKETBASE_URL: ${{ vars.POCKETBASE_URL }}
POCKETBASE_COLLECTION: ${{ vars.POCKETBASE_COLLECTION }}
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
PR_URL: ${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.pull_request.number }}
run: |
node << 'ENDSCRIPT'
(async function() {
const fs = require('fs');
const https = require('https');
const http = require('http');
const url = require('url');
function request(fullUrl, opts, redirectsLeft) {
if (redirectsLeft === undefined) redirectsLeft = 5;
return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:';
const body = opts.body;
const options = {
hostname: u.hostname,
port: u.port || (isHttps ? 443 : 80),
path: u.path,
method: opts.method || 'GET',
headers: opts.headers || {}
};
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http;
const req = lib.request(options, function(res) {
// Follow redirects (301/302/307/308)
if ([301, 302, 307, 308].indexOf(res.statusCode) !== -1 && res.headers.location && redirectsLeft > 0) {
res.resume();
const nextUrl = url.resolve(fullUrl, res.headers.location);
// For 301/302, browsers historically downgrade to GET; preserve method for 307/308.
const nextOpts = Object.assign({}, opts);
if (res.statusCode === 301 || res.statusCode === 302) {
nextOpts.method = 'GET';
delete nextOpts.body;
}
resolve(request(nextUrl, nextOpts, redirectsLeft - 1));
return;
}
let data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() {
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
const coll = process.env.POCKETBASE_COLLECTION;
const slugsText = fs.readFileSync('pocketbase_slugs.txt', 'utf8').trim();
const slugs = slugsText ? slugsText.split(/\s+/).filter(Boolean) : [];
if (slugs.length === 0) {
console.log('No slugs to update.');
return;
}
const authUrl = apiBase + '/collections/users/auth-with-password';
const authBody = JSON.stringify({
identity: process.env.POCKETBASE_ADMIN_EMAIL,
password: process.env.POCKETBASE_ADMIN_PASSWORD
});
const authRes = await request(authUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: authBody
});
if (!authRes.ok) {
throw new Error('Auth failed: ' + authRes.body);
}
const token = JSON.parse(authRes.body).token;
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
const prUrl = process.env.PR_URL || '';
for (const slug of slugs) {
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
const record = list.items && list.items[0];
if (!record) {
console.log('Slug not in DB, skipping: ' + slug);
continue;
}
const patchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: record.name || record.slug,
last_update_commit: prUrl,
is_dev: false
})
});
if (!patchRes.ok) {
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
continue;
}
console.log('Set is_dev=false for slug: ' + slug);
}
console.log('Done.');
})().catch(e => { console.error(e); process.exit(1); });
ENDSCRIPT
shell: bash