Skip to content

🤖 ci: pin all GitHub Actions to SHA hashes #3611

🤖 ci: pin all GitHub Actions to SHA hashes

🤖 ci: pin all GitHub Actions to SHA hashes #3611

Workflow file for this run

name: PR
on:
push:
branches: [main]
pull_request:
branches: ["**"]
merge_group:
workflow_dispatch:
inputs:
test_filter:
description: 'Optional test filter (e.g., "workspace", "tests/file.test.ts", or "-t pattern")'
required: false
type: string
concurrency:
# For PRs: group by PR number so pushes to same PR cancel previous runs
# For main: use unique SHA so builds never cancel each other (we want all artifacts)
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Permissions for GCP workload identity authentication (Windows code signing on main)
permissions:
contents: read
id-token: write
jobs:
# Detect what files changed to determine which jobs/tests to run
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
src: ${{ steps.filter.outputs.src }}
config: ${{ steps.filter.outputs.config }}
backend: ${{ steps.filter.outputs.backend }}
browser: ${{ steps.filter.outputs.browser }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
src:
- 'src/**'
- 'tests/**'
- 'vscode/**'
backend:
- 'src/node/**'
- 'src/cli/**'
- 'src/desktop/**'
- 'src/common/**'
- 'tests/ipc/**'
- 'tests/runtime/**'
browser:
- 'src/browser/**'
- 'tests/ui/**'
config:
- '.github/**'
- 'jest.config.cjs'
- 'babel.config.cjs'
- 'package.json'
- 'bun.lockb'
- 'tsconfig*.json'
- 'vite*.ts'
- 'Makefile'
- 'electron-builder.yml'
static-check:
name: Static Checks
if: github.event_name != 'push' || github.actor != 'github-merge-queue[bot]'
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-mux
- run: ./scripts/generate-version.sh
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.local/bin/shfmt
key: ${{ runner.os }}-shfmt-latest
- name: Install shfmt
run: |
set -euo pipefail
if [[ ! -f "$HOME/.local/bin/shfmt" ]] || ! "$HOME/.local/bin/shfmt" --version >/dev/null 2>&1; then
# webinstall.dev can be flaky; retry and force HTTP/1.1 to avoid HTTP/2 stream errors.
curl --retry 5 --retry-all-errors --retry-delay 2 -LsSf --http1.1 https://webinstall.dev/shfmt | bash
fi
if ! "$HOME/.local/bin/shfmt" --version >/dev/null 2>&1; then
echo "Error: shfmt install failed"
exit 1
fi
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install shellcheck
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y shellcheck
- uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
with:
extra_nix_config: |
experimental-features = nix-command flakes
- run: |
set -euo pipefail
curl --retry 5 --retry-all-errors --retry-delay 2 -LsSf https://astral.sh/uv/install.sh | sh
- run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: make -j static-check
env:
# Used by zizmor to lint third-party GitHub actions
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
test-unit:
name: Test / Unit
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-mux
- run: make build-main
# workflow_dispatch inputs are only triggerable by repo members, so direct
# interpolation is acceptable and preserves shell quoting in the filter.
- run: bun test --coverage --coverage-reporter=lcov ${{ github.event.inputs.test_filter || 'src' }} # zizmor: ignore[template-injection]
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
flags: unit-tests
fail_ci_if_error: false
test-integration:
name: Test / Integration
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
timeout-minutes: 10
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-mux
- run: make build-main
# workflow_dispatch inputs are only triggerable by repo members, so direct
# interpolation is acceptable and preserves shell quoting in the filter.
- name: Run integration tests
run: | # zizmor: ignore[template-injection]
set -euo pipefail
# Manual override (workflow_dispatch input)
if [[ -n "${{ github.event.inputs.test_filter }}" ]]; then
TEST_INTEGRATION=1 bun x jest --coverage --maxWorkers=100% --silent ${{ github.event.inputs.test_filter }}
exit 0
fi
# Skip if no relevant changes
if [[ "$BACKEND" != "true" && "$BROWSER" != "true" ]]; then
echo "No backend or browser changes - skipping integration tests"
exit 0
fi
# Backend changed: run ALL integration tests (includes tests/ui)
if [[ "$BACKEND" == "true" ]]; then
echo "Backend changes detected - running all integration tests"
TEST_INTEGRATION=1 bun x jest --coverage --maxWorkers=100% --silent tests/
else
# Browser-only changes: run tests/ui
echo "Browser changes detected - running tests/ui"
TEST_INTEGRATION=1 bun x jest --coverage --maxWorkers=100% --silent tests/ui/
fi
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
BACKEND: ${{ needs.changes.outputs.backend }}
BROWSER: ${{ needs.changes.outputs.browser }}
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
flags: integration-tests
fail_ci_if_error: false
test-storybook:
name: Test / Storybook
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && github.event.inputs.test_filter == '' && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-mux
- uses: ./.github/actions/setup-playwright
- run: make storybook-build
- run: |
bun x http-server storybook-static -p 6006 &
for i in {1..30}; do
if curl -sf http://127.0.0.1:6006 >/dev/null; then
echo "Storybook ready"
break
fi
echo "Waiting for Storybook... ($i/30)"
sleep 0.5
done
# Fail the step if Storybook never became reachable.
curl -sf http://127.0.0.1:6006 >/dev/null
- run: make test-storybook
test-e2e:
name: Test / E2E (${{ matrix.os }})
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && github.event.inputs.test_filter == '' && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
strategy:
fail-fast: false
matrix:
include:
- os: linux
runner: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
- os: macos
runner: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }}
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-mux
- name: Install xvfb
if: matrix.os == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y xvfb
- uses: ./.github/actions/setup-playwright
- name: Run tests
if: matrix.os == 'linux'
run: xvfb-run -a make test-e2e
env:
ELECTRON_DISABLE_SANDBOX: 1
- name: Run tests
if: matrix.os == 'macos'
run: make test-e2e PLAYWRIGHT_ARGS="tests/e2e/scenarios/windowLifecycle.spec.ts"
smoke-server:
name: Smoke / Server
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-mux
- run: make build
- run: npm pack
- env:
SERVER_PORT: 3001
SERVER_HOST: localhost
STARTUP_TIMEOUT: 30
run: |
# shellcheck disable=SC2012 # ls is fine here - known filename pattern in controlled directory
TARBALL=$(ls mux-*.tgz | head -1)
PACKAGE_TARBALL="$TARBALL" ./scripts/smoke-test.sh
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: failure()
with:
name: smoke-server-logs
path: /tmp/tmp.*/server.log
if-no-files-found: warn
retention-days: 7
smoke-docker:
name: Smoke / Docker
# Only run on merge queue (not every PR, not on merge-queue push to main)
if: github.event_name == 'merge_group' || (github.event_name == 'push' && github.actor != 'github-merge-queue[bot]')
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
load: true
tags: mux-server:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Test container
run: |
docker run -d --name mux-test -p 3000:3000 mux-server:test
for i in {1..30}; do
curl -sf http://localhost:3000/health && break
echo "Waiting... ($i/30)"
sleep 1
done
curl -sf http://localhost:3000/health | grep -q '"status":"ok"'
curl -sf http://localhost:3000/version | grep -q '"mode":"server"'
- if: failure()
run: docker logs mux-test
- if: always()
run: docker rm -f mux-test || true
build-linux:
name: Build / Linux
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-mux
- run: bun run build
- run: make dist-linux
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-linux
path: release/*.AppImage
retention-days: 30
if-no-files-found: error
build-macos:
name: Build / macOS
needs: [changes]
if: ${{ needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-15' || 'macos-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-mux
- name: Setup code signing
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: ./scripts/setup-macos-signing.sh
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
AC_APIKEY_ID: ${{ secrets.AC_APIKEY_ID }}
AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }}
- run: make dist-mac
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-macos-x64
path: release/*-x64.dmg
retention-days: 30
if-no-files-found: error
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-macos-arm64
path: release/*-arm64.dmg
retention-days: 30
if-no-files-found: error
build-windows:
name: Build / Windows
needs: [changes]
# Windows builds are slow - only run in merge queue and main pushes, not on every PR push
if: ${{ (github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') }}
runs-on: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-mux
- name: Add MSYS2 make to PATH
shell: pwsh
run: |
# MSYS2 is pre-installed on Windows runners, just needs to be in PATH
# Install make via pacman (faster than choco)
C:\msys64\usr\bin\pacman.exe -S --noconfirm make
echo "C:\msys64\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Verify tools
shell: bash
run: |
make --version
bun --version
magick --version | head -1
- run: bun run build
- name: Setup code signing
id: signing
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: ./.github/actions/setup-windows-signing
with:
ev_signing_cert: ${{ secrets.EV_SIGNING_CERT }}
gcp_workload_id_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
gcp_service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
- name: Package for Windows
run: make dist-win
env:
# EV signing environment variables (used by custom sign script if configured)
EV_KEYSTORE: ${{ vars.EV_KEYSTORE }}
EV_KEY: ${{ vars.EV_KEY }}
EV_TSA_URL: ${{ vars.EV_TSA_URL }}
GCLOUD_ACCESS_TOKEN: ${{ steps.signing.outputs.gcloud_access_token }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-windows
path: release/*.exe
retention-days: 30
if-no-files-found: error
build-vscode:
name: Build / VS Code
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-mux
- uses: ./.github/actions/build-vscode-extension
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-vscode
path: vscode/mux-*.vsix
retention-days: 30
if-no-files-found: error
codex-comments:
name: Codex Comments
if: github.event_name == 'pull_request'
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./scripts/check_codex_comments.sh ${{ github.event.pull_request.number }}
# Single required check - configure branch protection to require only this job
required:
name: Required
if: ${{ always() && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
needs:
- changes
- static-check
- test-unit
- test-integration
- test-storybook
- test-e2e
- smoke-server
- smoke-docker
- build-linux
- build-macos
- build-windows
- build-vscode
- codex-comments
runs-on: ubuntu-latest
steps:
- run: |
results="${{ join(needs.*.result, ' ') }}"
echo "Job results: $results"
for result in $results; do
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "❌ CI failed"
exit 1
fi
done
echo "✅ All checks passed"