Files
Cleanuparr/.github/workflows/pr-build.yml

299 lines
10 KiB
YAML

name: PR Build (Comment Triggered)
on:
issue_comment:
types: [created]
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
if: github.event.issue.pull_request != null
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
outputs:
build: ${{ steps.parse.outputs.build }}
meta: ${{ steps.parse.outputs.meta }}
pr_ref: ${{ steps.parse.outputs.pr_ref }}
pr_sha: ${{ steps.parse.outputs.pr_sha }}
pr_number: ${{ steps.parse.outputs.pr_number }}
steps:
- name: Parse command and check permissions
id: parse
uses: actions/github-script@v9
with:
script: |
const comment = context.payload.comment.body.trim();
const ALLOWED_USERS = ['flaminel'];
// Parse supported commands
const commands = {
'/build-windows': {
target: 'windows',
label: 'Windows installer',
artifacts: '`Cleanuparr-windows-installer`'
},
'/build-linux': {
target: 'linux',
label: 'Linux executables',
artifacts: '`executable-linux-amd64` and `executable-linux-arm64`'
},
'/test': {
target: 'test',
label: 'Unit tests',
artifacts: '`test-results`, `coverage-report` and `frontend-coverage-report`'
},
'/e2e': {
target: 'e2e',
label: 'E2E tests',
artifacts: '`e2e-test-results-core` and `e2e-test-results-clients`'
},
'/build-docker': {
target: 'docker',
label: 'Docker image',
artifacts: ''
}
};
const command = commands[comment];
if (!command) {
console.log(`Comment "${comment}" is not a recognized build command, skipping.`);
return;
}
const login = context.payload.comment.user.login;
if (!ALLOWED_USERS.includes(login.toLowerCase())) {
console.log(`User ${login} is not allowed to trigger builds, skipping.`);
return;
}
try {
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes'
});
} catch (e) {
console.log(`Could not add reaction: ${e}`);
}
// Fetch PR details
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
// Verify PR is open
if (pr.data.state !== 'open') {
console.log('PR is not open, skipping.');
return;
}
console.log(`${login} triggered ${comment} on PR #${pr.data.number} @ ${pr.data.head.sha}.`);
try {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: pr.data.head.sha,
state: 'pending',
context: `pr-build / ${command.target}`,
description: `${command.label} running`,
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
});
} catch (e) {
console.log(`Could not create commit status: ${e}`);
}
core.setOutput('build', command.target);
core.setOutput('meta', JSON.stringify({ label: command.label, artifacts: command.artifacts }));
// Export PR details for downstream jobs
core.setOutput('pr_ref', pr.data.head.ref);
core.setOutput('pr_sha', pr.data.head.sha);
core.setOutput('pr_number', String(pr.data.number));
build-frontend:
needs: validate
if: needs.validate.outputs.build == 'windows' || needs.validate.outputs.build == 'linux'
uses: ./.github/workflows/build-frontend.yml
with:
ref: ${{ needs.validate.outputs.pr_sha }}
secrets: inherit
build-windows:
needs: [validate, build-frontend]
if: needs.validate.outputs.build == 'windows'
concurrency:
group: pr-build-windows-${{ needs.validate.outputs.pr_number }}
cancel-in-progress: true
uses: ./.github/workflows/build-windows-installer.yml
with:
ref: ${{ needs.validate.outputs.pr_sha }}
secrets: inherit
build-linux:
needs: [validate, build-frontend]
if: needs.validate.outputs.build == 'linux'
concurrency:
group: pr-build-linux-${{ needs.validate.outputs.pr_number }}
cancel-in-progress: true
uses: ./.github/workflows/build-executable.yml
with:
ref: ${{ needs.validate.outputs.pr_sha }}
platforms: '[{"runtime":"linux-x64","platform":"linux-amd64"},{"runtime":"linux-arm64","platform":"linux-arm64"}]'
secrets: inherit
test:
needs: validate
if: needs.validate.outputs.build == 'test'
concurrency:
group: pr-test-${{ needs.validate.outputs.pr_number }}
cancel-in-progress: true
uses: ./.github/workflows/test.yml
with:
ref: ${{ needs.validate.outputs.pr_sha }}
secrets: inherit
e2e:
needs: validate
if: needs.validate.outputs.build == 'e2e'
concurrency:
group: pr-e2e-${{ needs.validate.outputs.pr_number }}
cancel-in-progress: true
uses: ./.github/workflows/e2e.yml
with:
ref: ${{ needs.validate.outputs.pr_sha }}
secrets: inherit
build-docker:
needs: validate
if: needs.validate.outputs.build == 'docker'
permissions:
contents: read
packages: write
uses: ./.github/workflows/build-docker.yml
with:
ref: ${{ needs.validate.outputs.pr_sha }}
pr_number: ${{ needs.validate.outputs.pr_number }}
push_docker: true
secrets: inherit
post-result:
needs: [validate, build-frontend, build-windows, build-linux, test, e2e, build-docker]
if: always() && needs.validate.outputs.build != ''
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
steps:
- name: Post result comment
uses: actions/github-script@v9
env:
PR_REF: ${{ needs.validate.outputs.pr_ref }}
META: ${{ needs.validate.outputs.meta }}
with:
script: |
const target = '${{ needs.validate.outputs.build }}';
// needs.* is only readable inside this job, so the result lookup has to live here.
const results = {
windows: '${{ needs.build-windows.result }}',
linux: '${{ needs.build-linux.result }}',
test: '${{ needs.test.result }}',
e2e: '${{ needs.e2e.result }}',
docker: '${{ needs.build-docker.result }}'
};
const frontend = '${{ needs.build-frontend.result }}';
let result = results[target];
if (result === 'skipped' && (frontend === 'failure' || frontend === 'cancelled')) {
result = frontend;
}
if (!result || result === 'skipped') {
console.log(`Nothing to report for target "${target}" (${result}).`);
return;
}
const { label, artifacts } = JSON.parse(process.env.META);
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const prSha = '${{ needs.validate.outputs.pr_sha }}';
const statusContext = `pr-build / ${target}`;
let publishStatus = result !== 'cancelled';
if (publishStatus) {
try {
const combined = await github.rest.repos.getCombinedStatusForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: prSha
});
const current = combined.data.statuses.find(s => s.context === statusContext);
publishStatus = !current || current.state !== 'pending' || current.target_url === runUrl;
} catch (e) {
console.log(`Could not read the current commit status: ${e}`);
}
}
if (publishStatus) {
try {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: prSha,
state: result === 'success' ? 'success' : 'failure',
context: statusContext,
description: `${label} ${result}`,
target_url: runUrl
});
} catch (e) {
console.log(`Could not update commit status: ${e}`);
}
} else {
console.log(`A newer run owns "${statusContext}", skipping the status update.`);
}
const prRef = process.env.PR_REF;
const prNumber = parseInt('${{ needs.validate.outputs.pr_number }}');
const shortSha = prSha.substring(0, 7);
let body;
if (result === 'success') {
const how = target === 'docker'
? [`**Pull:** \`docker pull ghcr.io/cleanuparr/cleanuparr:pr-${prNumber}\``]
: [
`**Download:** open the [workflow run](${runUrl}), scroll to the **Artifacts** section at the bottom.`,
`The ${artifacts} artifact(s) are attached to that run.`
];
body = [`${label} **succeeded** for \`${prRef}\` (\`${shortSha}\`).`, ``, ...how].join('\n');
} else {
const verb = result === 'cancelled' ? 'was **cancelled**' : '**failed**';
body = [
`${label} ${verb} for \`${prRef}\` (\`${shortSha}\`).`,
``,
`See the [workflow run](${runUrl}) for details.`
].join('\n');
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body
});