Skip to content

feat: Add GitHub Actions CI/CD workflow #1

feat: Add GitHub Actions CI/CD workflow

feat: Add GitHub Actions CI/CD workflow #1

Workflow file for this run

name: Build Binaries
# Prevent concurrent runs on the same branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_run:
workflows: ["build"]
types: [completed]
branches: [main, develop]
workflow_dispatch:
permissions:
contents: read
actions: read
checks: read
env:
GO_VERSION: "1.25.1"
jobs:
build-binaries:
name: Build Multi-Platform Binaries
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event_name == 'pull_request' || (github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'workflow_dispatch')) }}
strategy:
matrix:
include:
- os: linux
arch: amd64
output: github-repo-linux-amd64
- os: linux
arch: arm64
output: github-repo-linux-arm64
- os: darwin
arch: amd64
output: github-repo-darwin-amd64
- os: darwin
arch: arm64
output: github-repo-darwin-arm64
- os: windows
arch: amd64
output: github-repo-windows-amd64.exe
steps:
- name: Check if build and lint workflows passed
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const sha = context.sha;
const shaPattern = /^[0-9a-f]{40}$/i;
if (!sha || !shaPattern.test(sha)) {
core.setFailed(`Invalid SHA format: ${sha}`);
return;
}
// Check build workflow
const { data: buildRuns } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build.yaml',
head_sha: sha,
per_page: 1
});
// Check lint workflow
const { data: lintRuns } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'lint.yaml',
head_sha: sha,
per_page: 1
});
if (buildRuns.workflow_runs.length === 0 || buildRuns.workflow_runs[0].conclusion !== 'success') {
core.setFailed('Build workflow did not pass');
return;
}
if (lintRuns.workflow_runs.length === 0 || lintRuns.workflow_runs[0].conclusion !== 'success') {
core.setFailed('Lint workflow did not pass');
return;
}
core.info('✅ Build and lint workflows passed');
- name: Validate workflow_run security
if: github.event_name == 'workflow_run'
uses: actions/github-script@v7
with:
script: |
// Security check: Ensure workflow_run is from same repository (not a fork)
const expectedRepo = context.repo;
const workflowRun = context.payload.workflow_run;
if (workflowRun.repository.full_name !== `${expectedRepo.owner}/${expectedRepo.repo}`) {
core.setFailed(`Security: workflow_run triggered from foreign repository: ${workflowRun.repository.full_name}`);
return;
}
// Verify the workflow is from our trusted workflows
const trustedWorkflows = ['build.yaml', 'build.yml'];
const workflowName = workflowRun.name || workflowRun.workflow_id;
if (!trustedWorkflows.some(wf => workflowName.includes(wf) || workflowRun.path.includes(wf))) {
core.setFailed(`Security: workflow_run from untrusted workflow: ${workflowName}`);
return;
}
core.info(`✅ Security check passed: workflow_run from ${workflowRun.repository.full_name}`);
- name: Check if lint workflow passed
if: github.event_name == 'workflow_run'
uses: actions/github-script@v7
env:
HEAD_SHA: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.workflow_run.head_sha }}
with:
script: |
// Validate SHA format (40 hex characters)
const shaPattern = /^[0-9a-f]{40}$/i;
const headSha = process.env.HEAD_SHA;
if (!headSha || !shaPattern.test(headSha)) {
core.setFailed(`Invalid SHA format: ${headSha}`);
return;
}
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'lint.yaml',
head_sha: headSha,
per_page: 1
});
if (runs.workflow_runs.length > 0 && runs.workflow_runs[0].conclusion !== 'success') {
core.setFailed('Lint workflow did not pass');
}
- name: Checkout code
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Build binary for ${{ matrix.os }}/${{ matrix.arch }}
env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
CGO_ENABLED: 0
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || (github.event_name == 'push' || github.event_name == 'pull_request') && github.ref_name || github.event.workflow_run.head_branch || 'unknown' }}
COMMIT_HASH: ${{ github.event_name == 'workflow_dispatch' && github.sha || (github.event_name == 'push' || github.event_name == 'pull_request') && github.sha || github.event.workflow_run.head_sha || github.sha }}
OUTPUT_NAME: ${{ matrix.output }}
run: |
go build -ldflags="-s -w -X 'main.Version=${VERSION}' -X 'main.GitCommitHash=${COMMIT_HASH}' -X 'main.BuiltAt=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" -o "${OUTPUT_NAME}"
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.output }}
path: ${{ matrix.output }}
retention-days: 30