name: CI/CD on: # only on PRs into and merge to default branch pull_request: branches: - master push: branches: - master jobs: ci_unit: name: Run Unit Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Run Unit Tests run: npm test - uses: codecov/codecov-action@v5 with: directory: ./coverage/ verbose: true ci_integration: name: Run Integration Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: happy-path id: happy_path uses: ./ with: timeout_minutes: 1 max_attempts: 2 command: npm -v - uses: nick-fields/assert-action@v2 with: expected: true actual: ${{ steps.happy_path.outputs.total_attempts == '1' && steps.happy_path.outputs.exit_code == '0' }} - name: log examples uses: ./ with: command: node ./.github/scripts/log-examples.js timeout_minutes: 1 - name: sad-path (error) id: sad_path_error uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 2 command: node -e "process.exit(1)" - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.sad_path_error.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.sad_path_error.outcome }} - name: retry_on (timeout) fails early if error encountered id: retry_on_timeout_fail uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 3 retry_on: timeout command: node -e "process.exit(2)" - uses: nick-fields/assert-action@v2 with: expected: 1 actual: ${{ steps.retry_on_timeout_fail.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.retry_on_timeout_fail.outcome }} - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.retry_on_timeout_fail.outputs.exit_code }} - name: retry_on (error) id: retry_on_error uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 2 retry_on: error command: node -e "process.exit(2)" - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.retry_on_error.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.retry_on_error.outcome }} - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.retry_on_error.outputs.exit_code }} - name: sad-path (wrong shell for OS) id: wrong_shell uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 2 shell: cmd command: 'dir' - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.wrong_shell.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.wrong_shell.outcome }} ci_integration_envvar: name: Run Integration Env Var Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: env-vars-passed-through uses: ./ env: NODE_OPTIONS: '--max_old_space_size=3072' with: timeout_minutes: 1 max_attempts: 2 command: node -e 'console.log(process.env.NODE_OPTIONS)' ci_integration_large_output: name: Run Integration Large Output Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Test 100MiB of output can be processed id: large-output continue-on-error: true uses: ./ with: max_attempts: 1 timeout_minutes: 5 command: 'make -C ./test-data/large-output bytes-102400' - name: Assert test had expected result uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.large-output.outcome }} - name: Assert exit code is expected uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.large-output.outputs.exit_code }} ci_integration_retry_on_exit_code: name: Run Integration retry_on_exit_code Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: retry_on_exit_code (with expected error code) id: retry_on_exit_code_expected uses: ./ continue-on-error: true with: timeout_minutes: 1 retry_on_exit_code: 2 max_attempts: 3 command: node -e "process.exit(2)" - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.retry_on_exit_code_expected.outcome }} - uses: nick-fields/assert-action@v2 with: expected: 3 actual: ${{ steps.retry_on_exit_code_expected.outputs.total_attempts }} - name: retry_on_exit_code (with unexpected error code) id: retry_on_exit_code_unexpected uses: ./ continue-on-error: true with: timeout_minutes: 1 retry_on_exit_code: 2 max_attempts: 3 command: node -e "process.exit(1)" - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.retry_on_exit_code_unexpected.outcome }} - uses: nick-fields/assert-action@v2 with: expected: 1 actual: ${{ steps.retry_on_exit_code_unexpected.outputs.total_attempts }} ci_integration_continue_on_error: name: Run Integration continue_on_error Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: happy-path (continue_on_error) id: happy_path_continue_on_error uses: ./ with: command: node -e "process.exit(0)" timeout_minutes: 1 continue_on_error: true - name: sad-path (continue_on_error) id: sad_path_continue_on_error uses: ./ with: command: node -e "process.exit(33)" timeout_minutes: 1 continue_on_error: true - name: Verify continue_on_error returns correct exit code on success uses: nick-fields/assert-action@v2 with: expected: 0 actual: ${{ steps.happy_path_continue_on_error.outputs.exit_code }} - name: Verify continue_on_error exits with correct outcome on success uses: nick-fields/assert-action@v2 with: expected: success actual: ${{ steps.happy_path_continue_on_error.outcome }} - name: Verify continue_on_error returns correct exit code on error uses: nick-fields/assert-action@v2 with: expected: 33 actual: ${{ steps.sad_path_continue_on_error.outputs.exit_code }} - name: Verify continue_on_error exits with successful outcome when an error occurs uses: nick-fields/assert-action@v2 with: expected: success actual: ${{ steps.sad_path_continue_on_error.outcome }} ci_integration_retry_wait_seconds: name: Run Integration Tests (retry_wait_seconds) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: sad-path (retry_wait_seconds) id: sad_path_wait_sec uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 3 retry_wait_seconds: 15 command: npm install this-isnt-a-real-package-name-zzz - uses: nick-fields/assert-action@v2 with: expected: 3 actual: ${{ steps.sad_path_wait_sec.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.sad_path_wait_sec.outcome }} - uses: nick-fields/assert-action@v2 with: expected: 'Final attempt failed' actual: ${{ steps.sad_path_wait_sec.outputs.exit_error }} comparison: contains ci_integration_on_retry_cmd: name: Run Integration Tests (on_retry_command) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: new-command-on-retry id: new-command-on-retry uses: ./ with: timeout_minutes: 1 max_attempts: 3 command: node -e "process.exit(1)" new_command_on_retry: node -e "console.log('this is the new command on retry')" - name: on-retry-cmd id: on-retry-cmd uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 3 command: node -e "process.exit(1)" on_retry_command: node -e "console.log('this is a retry command')" - name: on-retry-cmd (on-retry fails) id: on-retry-cmd-fails uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 3 command: node -e "process.exit(1)" on_retry_command: node -e "throw new Error('This is an on-retry command error')" # timeout tests take longer to run so run in parallel ci_integration_timeout_seconds: name: Run Integration Timeout Tests (seconds) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: sad-path (timeout) id: sad_path_timeout uses: ./ continue-on-error: true with: timeout_seconds: 15 max_attempts: 2 command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()" - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.sad_path_timeout.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.sad_path_timeout.outcome }} ci_integration_timeout_retry_on_timeout: name: Run Integration Timeout Tests (retry_on timeout) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: retry_on (timeout) id: retry_on_timeout uses: ./ continue-on-error: true with: timeout_seconds: 15 max_attempts: 2 retry_on: timeout command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()" - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.retry_on_timeout.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.retry_on_timeout.outcome }} ci_integration_timeout_retry_on_error: name: Run Integration Timeout Tests (retry_on error) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: retry_on (error) fails early if timeout encountered id: retry_on_error_fail uses: ./ continue-on-error: true with: timeout_seconds: 15 max_attempts: 2 retry_on: error command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()" - uses: nick-fields/assert-action@v2 with: expected: 1 actual: ${{ steps.retry_on_error_fail.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.retry_on_error_fail.outcome }} - uses: nick-fields/assert-action@v2 with: expected: 1 actual: ${{ steps.retry_on_error_fail.outputs.exit_code }} ci_integration_timeout_minutes: name: Run Integration Timeout Tests (minutes) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: sad-path (timeout minutes) id: sad_path_timeout_minutes uses: ./ continue-on-error: true with: timeout_minutes: 1 max_attempts: 2 command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()" - uses: nick-fields/assert-action@v2 with: expected: 2 actual: ${{ steps.sad_path_timeout_minutes.outputs.total_attempts }} - uses: nick-fields/assert-action@v2 with: expected: failure actual: ${{ steps.sad_path_timeout_minutes.outcome }} ci_windows: name: Run Windows Tests runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Powershell test uses: ./ with: timeout_minutes: 1 max_attempts: 2 shell: powershell command: Get-ComputerInfo - name: CMD.exe test uses: ./ with: timeout_minutes: 1 max_attempts: 2 shell: cmd command: echo %PATH% - name: Python test uses: ./ with: timeout_minutes: 1 max_attempts: 2 shell: python command: print('1', '2', '3') - name: Multi-line multi-command Test uses: ./ with: timeout_minutes: 1 max_attempts: 2 command: | Get-ComputerInfo Get-Date - name: Multi-line single-command Test uses: ./ with: timeout_minutes: 1 max_attempts: 2 shell: cmd command: >- echo "this is a test" ci_all_tests_passed: name: All tests passed needs: [ ci_unit, ci_integration, ci_integration_envvar, ci_integration_large_output, ci_integration_on_retry_cmd, ci_integration_retry_wait_seconds, ci_integration_continue_on_error, ci_integration_retry_on_exit_code, ci_integration_timeout_seconds, ci_integration_timeout_minutes, ci_integration_timeout_retry_on_timeout, ci_integration_timeout_retry_on_error, ci_windows, ] runs-on: ubuntu-latest steps: - run: echo "If this is hit, all tests successfully passed" # runs on merge to default only cd: name: Publish Action needs: [ci_all_tests_passed] if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Release id: semantic uses: cycjimmy/semantic-release-action@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Tag # only bump v# (e.g., v3) tag if semantic release action publishes any new version if: ${{ steps.semantic.outputs.new_release_major_version != '' }} run: git tag -f v${MAJOR_VERSION} && git push -f origin v${MAJOR_VERSION} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MAJOR_VERSION: ${{ steps.semantic.outputs.new_release_major_version }}