Skip to content

chore(deps): update anthropics/claude-code-action action to v1.0.210 #357

chore(deps): update anthropics/claude-code-action action to v1.0.210

chore(deps): update anthropics/claude-code-action action to v1.0.210 #357

Workflow file for this run

name: PR Build Test
on:
issue_comment:
types: [created]
# Cancel any in-progress runs for the same PR when a new /build-test is triggered
concurrency:
group: build-test-pr-${{ github.event.issue.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
id-token: write
jobs:
# Gate job to check if we should run the build
check-trigger:
name: Check Trigger
runs-on: ubuntu-latest
if: github.event.issue.pull_request && contains(github.event.comment.body, '/build-test')
outputs:
pr_number: ${{ steps.pr-info.outputs.pr_number }}
pr_sha: ${{ steps.pr-info.outputs.pr_sha }}
sign_windows: ${{ steps.parse-flags.outputs.sign_windows }}
sign_windows_digicert: ${{ steps.parse-flags.outputs.sign_windows_digicert }}
prerelease: ${{ steps.parse-flags.outputs.prerelease }}
comment_id: ${{ steps.react.outputs.comment_id }}
steps:
- name: Parse command flags
id: parse-flags
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const comment = context.payload.comment.body;
const flags = comment.split(/\s+/);
const signWindowsDigicert = flags.includes('--sign-windows-digicert');
const signWindowsAzure = flags.includes('--sign-windows');
const signWindows = signWindowsAzure && !signWindowsDigicert;
const prerelease = flags.includes('--prerelease');
core.setOutput('sign_windows', signWindows ? 'true' : 'false');
core.setOutput('sign_windows_digicert', signWindowsDigicert ? 'true' : 'false');
core.setOutput('prerelease', prerelease ? 'true' : 'false');
if (signWindowsDigicert) {
core.info('🔐 Windows signing enabled via --sign-windows-digicert flag (DigiCert KeyLocker)');
if (signWindowsAzure) {
core.warning('Both --sign-windows and --sign-windows-digicert were specified; using DigiCert and skipping Azure');
}
} else if (signWindows) {
core.info('🔐 Windows signing enabled via --sign-windows flag (Azure Trusted Signing)');
}
if (prerelease) {
core.info('🧪 Pre-release build enabled via --prerelease flag');
}
- name: Check write permission
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const commenter = context.payload.comment.user.login;
const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter
});
const allowed = ['admin', 'write', 'maintain'].includes(permissionLevel.permission);
if (!allowed) {
core.setFailed(`❌ ${commenter} does not have write access. Only collaborators with write permission can trigger builds.`);
return;
}
core.info(`✅ ${commenter} has ${permissionLevel.permission} permission`);
- name: React to comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
id: react
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes'
});
core.setOutput('comment_id', context.payload.comment.id);
- name: Get PR info
id: pr-info
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const isFork = pr.data.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
if (isFork) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.data.number,
body: '❌ `/build-test` is disabled for forked PRs to protect signing secrets. Please open a PR from a branch in this repository if you need a build.'
});
core.setFailed('❌ /build-test is disabled for forked PRs to protect signing secrets.');
return;
}
core.setOutput('pr_number', pr.data.number);
core.setOutput('pr_sha', pr.data.head.sha);
core.setOutput('base_ref', pr.data.base.ref);
- name: Check if branch is up-to-date with base
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const baseBranch = '${{ steps.pr-info.outputs.base_ref }}';
const headSha = '${{ steps.pr-info.outputs.pr_sha }}';
const prNumber = ${{ steps.pr-info.outputs.pr_number }};
// Compare base branch with PR head
const comparison = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: headSha,
head: baseBranch
});
// If base is ahead of the PR branch, it needs a rebase
if (comparison.data.ahead_by > 0) {
const behindCount = comparison.data.ahead_by;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `⚠️ This branch is **${behindCount} commit${behindCount > 1 ? 's' : ''} behind \`${baseBranch}\`**. Please rebase before running \`/build-test\` to ensure artifacts are built on the latest code.\n\n\`\`\`bash\ngit fetch origin ${baseBranch}\ngit rebase origin/${baseBranch}\ngit push --force-with-lease\n\`\`\``
});
core.setFailed(`Branch is ${behindCount} commits behind ${baseBranch}. Rebase required.`);
} else {
core.info(`✅ Branch is up-to-date with ${baseBranch}`);
}
build:
name: Build on ${{ matrix.os }}${{ matrix.arch && format(' ({0})', matrix.arch) || '' }}
needs: check-trigger
runs-on: ${{ matrix.os }}
# Activate the `artifact-signing` environment only for the Windows matrix
# row when Azure Trusted Signing is requested (`--sign-windows`). The Azure
# OIDC federated credential is scoped to
# `repo:stacklok/toolhive-studio:environment:artifact-signing`, so only
# the Windows signing job should run in that environment for the token
# exchange to succeed. DigiCert signing (`--sign-windows-digicert`) uses
# repo-level SM_* secrets and does not need this environment. Scoping to
# `matrix.platform == 'win32'` keeps the Linux/macOS matrix rows out of
# the environment so they don't receive environment-scoped signing secrets
# or trigger environment approvals. Leaving it empty for non-signing builds
# keeps them free of approvals.
environment: ${{ needs.check-trigger.outputs.sign_windows == 'true' && matrix.platform == 'win32' && 'artifact-signing' || '' }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04-arm
arch: arm64
platform: linux
- os: ubuntu-latest
arch: x64
platform: linux
- os: windows-latest
arch: x64
platform: win32
- os: macos-15-intel
arch: x64
platform: darwin
- os: macos-latest
arch: arm64
platform: darwin
env:
NODE_OPTIONS: '--max_old_space_size=4096'
PR_NUMBER: ${{ needs.check-trigger.outputs.pr_number }}
steps:
- name: Checkout PR branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.check-trigger.outputs.pr_sha }}
- name: Setup
uses: ./.github/actions/setup
- name: Set version
id: version
shell: bash
run: |
if [ "${{ needs.check-trigger.outputs.prerelease }}" = "true" ]; then
VERSION="0.0.0-beta.${{ env.PR_NUMBER }}"
else
VERSION="0.0.0-pr.${{ env.PR_NUMBER }}"
fi
echo "Setting version to: $VERSION"
npm version $VERSION --no-git-tag-version --allow-same-version
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "artifact_prefix=ToolHive-$VERSION" >> $GITHUB_OUTPUT
- name: Setup macOS code signing
uses: ./.github/actions/setup-macos-codesign
if: runner.os == 'macOS'
with:
apple-certificate: ${{ secrets.APPLE_CERTIFICATE }}
apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }}
apple-api-key: ${{ secrets.APPLE_API_KEY }}
apple-issuer-id: ${{ secrets.APPLE_ISSUER_ID }}
apple-key-id: ${{ secrets.APPLE_KEY_ID }}
- name: Setup Windows code signing (Azure Trusted Signing)
uses: ./.github/actions/setup-azure-trusted-signing
if: runner.os == 'Windows' && needs.check-trigger.outputs.sign_windows == 'true'
with:
azure-client-id: ${{ secrets.AZURE_ARTIFACT_SIGNING_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_ARTIFACT_SIGNING_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_ARTIFACT_SIGNING_SUBSCRIPTION_ID }}
azure-endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
azure-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }}
azure-certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}
- name: Setup Windows code signing (DigiCert KeyLocker)
uses: ./.github/actions/setup-windows-codesign
if: runner.os == 'Windows' && needs.check-trigger.outputs.sign_windows_digicert == 'true'
with:
sm-host: ${{ secrets.SM_HOST }}
sm-api-key: ${{ secrets.SM_API_KEY }}
sm-client-cert-file-b64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
sm-client-cert-password: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
sm-code-signing-cert-sha1-hash: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
- name: Setup Flatpak (Linux only)
uses: ./.github/actions/setup-flatpak
if: runner.os == 'Linux'
- name: Build App
run: pnpm run make
env:
npm_config_target_platform: ${{ matrix.platform }}
# macOS signing (set by composite action)
APPLE_API_KEY: ${{ env.APPLE_API_KEY_PATH }}
APPLE_ISSUER_ID: ${{ env.APPLE_ISSUER_ID }}
APPLE_KEY_ID: ${{ env.APPLE_KEY_ID }}
- name: Upload flatpak bundle as artifact (Linux only)
if: runner.os == 'Linux'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: flatpak-bundle-${{ matrix.arch == 'arm64' && 'aarch64' || 'x86_64' }}
path: out/make/**/*.flatpak
retention-days: 1
- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ steps.version.outputs.artifact_prefix }}-${{ matrix.platform }}-${{ matrix.arch }}
path: |
out/make/**/*.dmg
out/make/**/*.zip
out/make/**/*.exe
out/make/**/*.deb
out/make/**/*.rpm
out/make/**/*.tar.gz
out/make/**/*.flatpak
retention-days: 7
if-no-files-found: error
post-comment:
name: Post Download Links
needs: [check-trigger, build]
runs-on: ubuntu-latest
if: always() && needs.check-trigger.result == 'success'
steps:
- name: Get workflow run URL
id: run-url
run: |
echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
- name: Post comment with results
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const prNumber = ${{ needs.check-trigger.outputs.pr_number }};
const version = `pr-${prNumber}`;
const runUrl = `${{ steps.run-url.outputs.url }}`;
const buildResult = '${{ needs.build.result }}';
const triggeredBy = '${{ github.event.comment.user.login }}';
const signWindowsAzure = '${{ needs.check-trigger.outputs.sign_windows }}' === 'true';
const signWindowsDigicert = '${{ needs.check-trigger.outputs.sign_windows_digicert }}' === 'true';
const windowsSigningProvider = signWindowsDigicert
? 'DigiCert KeyLocker'
: signWindowsAzure
? 'Azure Trusted Signing'
: null;
// Marker to identify bot comments for this workflow
const marker = '<!-- build-test-bot -->';
const windowsSignedNote = windowsSigningProvider
? `macOS builds are signed (not notarized). Windows builds are signed and timestamped via ${windowsSigningProvider}. Linux builds are unsigned.`
: 'macOS builds are signed (not notarized). Windows and Linux builds are unsigned.';
let body;
if (buildResult === 'success') {
body = [
marker,
'## Build Artifacts for PR #' + prNumber,
'',
'| Platform | Architecture | Status |',
'|----------|--------------|--------|',
'| macOS | arm64 | :white_check_mark: Ready |',
'| macOS | x64 | :white_check_mark: Ready |',
'| Windows | arm64 | :white_check_mark: Ready' + (windowsSigningProvider ? ` (signed via ${windowsSigningProvider})` : '') + ' |',
'| Windows | x64 | :white_check_mark: Ready' + (windowsSigningProvider ? ` (signed via ${windowsSigningProvider})` : '') + ' |',
'| Linux | arm64 | :white_check_mark: Ready |',
'| Linux | x64 | :white_check_mark: Ready |',
'',
'**[Download artifacts from workflow run](' + runUrl + ')**',
'',
'*Version: `' + version + '` | Artifacts expire in 7 days | Triggered by @' + triggeredBy + '*',
'',
'> **Note:** ' + windowsSignedNote
].join('\n');
} else {
body = [
marker,
'## Build Failed for PR #' + prNumber,
'',
'The build encountered errors. Please check the [workflow run](' + runUrl + ') for details.',
'',
'*Triggered by @' + triggeredBy + '*'
].join('\n');
}
// Find existing bot comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
const botComment = comments.find(comment => comment.body.includes(marker));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
core.info(`Updated existing comment ${botComment.id}`);
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
core.info('Created new comment');
}
// Add rocket reaction to original comment on success
const commentId = '${{ needs.check-trigger.outputs.comment_id }}';
if (buildResult === 'success' && commentId) {
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'rocket'
});
}