feat: Add GitHub Actions CI/CD workflow #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |