Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
- name: Syntax check
run: |
bash -n scripts/setup.sh
bash -n scripts/test-setup.sh
sh -n installer/install.sh

- name: Install ShellCheck
Expand All @@ -33,6 +34,7 @@ jobs:
- name: ShellCheck
run: |
shellcheck --severity=style scripts/setup.sh
shellcheck --severity=style scripts/test-setup.sh
shellcheck --shell=sh --severity=style installer/install.sh

lint-powershell:
Expand All @@ -53,13 +55,14 @@ jobs:
shell: pwsh
run: |
Invoke-ScriptAnalyzer -Path scripts/setup.ps1 -Settings PSGallery -EnableExit
Invoke-ScriptAnalyzer -Path scripts/test-setup.ps1 -Settings PSGallery -EnableExit
Invoke-ScriptAnalyzer -Path installer/install.ps1 -Settings PSGallery -EnableExit

- name: AST parse check
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
foreach ($file in @("scripts/setup.ps1", "installer/install.ps1")) {
foreach ($file in @("scripts/setup.ps1", "scripts/test-setup.ps1", "installer/install.ps1")) {
$tokens = $null
$errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokens, [ref]$errors)
Expand All @@ -72,6 +75,30 @@ jobs:
Write-Output "OK: $file"
}

test-setup:
name: Setup regression tests (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Test Bash setup
if: runner.os != 'Windows'
shell: bash
run: bash scripts/test-setup.sh

- name: Test PowerShell setup
if: runner.os == 'Windows'
shell: pwsh
run: pwsh -NoProfile -File scripts/test-setup.ps1

installer-drift:
name: Vendored installer drift check
runs-on: ubuntu-latest
Expand Down Expand Up @@ -114,7 +141,7 @@ jobs:
# values are enough to test the install path without secrets.
- name: Install Cloudsmith CLI
id: setup
uses: ./
uses: $/
with:
oidc-namespace: placeholder-namespace
oidc-service-slug: placeholder-service
Expand Down Expand Up @@ -159,7 +186,7 @@ jobs:

- name: Install Cloudsmith CLI
id: setup
uses: ./
uses: $/
with:
# Latest version published to the pre-release test repository.
# Bump to the first production release when the vendored installer
Expand Down Expand Up @@ -198,19 +225,19 @@ jobs:
- name: No authentication inputs should fail
id: no-auth
continue-on-error: true
uses: ./
uses: $/

- name: Incomplete OIDC pair should fail
id: partial-oidc
continue-on-error: true
uses: ./
uses: $/
with:
oidc-namespace: placeholder-namespace

- name: verify-auth with a bad API key should fail
id: bad-key
continue-on-error: true
uses: ./
uses: $/
with:
api-key: placeholder-invalid-key
verify-auth: "true"
Expand Down Expand Up @@ -272,7 +299,7 @@ jobs:

- name: Install and authenticate
id: setup
uses: ./
uses: $/
with:
api-key: ${{ secrets.CLOUDSMITH_API_KEY }}
export-auth-token: ${{ matrix.export-auth-token }}
Expand Down Expand Up @@ -324,7 +351,7 @@ jobs:

- name: Install and authenticate
id: setup
uses: ./
uses: $/
with:
oidc-namespace: ${{ vars.CLOUDSMITH_NAMESPACE }}
oidc-service-slug: ${{ vars.CLOUDSMITH_SERVICE_SLUG }}
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Thank you for considering contributing to the Cloudsmith CLI Setup action!
bash -n scripts/setup.sh
shellcheck --severity=style scripts/setup.sh
pwsh -Command "Invoke-ScriptAnalyzer -Path scripts/setup.ps1 -Settings PSGallery -EnableExit"
bash scripts/test-setup.sh
pwsh -NoProfile -File scripts/test-setup.ps1
```

4. Commit, push to your fork, and open a pull request.
Expand Down
14 changes: 12 additions & 2 deletions scripts/setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,18 @@ $credentialUsername = ''
if ($exportAuthToken -eq 'true') {
# Resolve once: the helper performs the OIDC exchange when OIDC is the
# effective source and emits a versioned JSON credential document.
$credentialJsonLines = & $executable credential-helper generic
$credentialStatus = $LASTEXITCODE
$previousApiKey = $env:CLOUDSMITH_API_KEY
try {
# Ignore tokens exported by earlier invocations, but keep explicit API keys.
if ($hasOidcNamespace -and -not $hasApiKey) {
$env:CLOUDSMITH_API_KEY = $null
}
$credentialJsonLines = & $executable credential-helper generic
$credentialStatus = $LASTEXITCODE
}
finally {
$env:CLOUDSMITH_API_KEY = $previousApiKey
}
if ($credentialStatus -ne 0) {
throw "Failed to resolve credentials. 'export-auth-token' requires Cloudsmith CLI 1.21.0 or later and valid credentials."
}
Expand Down
8 changes: 7 additions & 1 deletion scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,13 @@ if [[ "$export_auth_token" == "true" ]]; then

# Resolve once: the helper performs the OIDC exchange when OIDC is the
# effective source and emits a versioned JSON credential document.
if ! credential_document="$("$executable" credential-helper generic)"; then
if ! credential_document="$(
# Ignore tokens exported by earlier invocations, but keep explicit API keys.
if [[ "$has_oidc_namespace" == "yes" && -z "$INPUT_API_KEY" ]]; then
unset CLOUDSMITH_API_KEY
fi
"$executable" credential-helper generic
)"; then
fail "Failed to resolve credentials. 'export-auth-token' requires Cloudsmith CLI 1.21.0 or later and valid credentials."
fi

Expand Down
118 changes: 118 additions & 0 deletions scripts/test-setup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
$ErrorActionPreference = 'Stop'
$setupScript = Join-Path $PSScriptRoot 'setup.ps1'
$testRoot = Join-Path ([IO.Path]::GetTempPath()) ("cloudsmith-setup-test-" + [Guid]::NewGuid().ToString('N'))

function Assert-Equal {
param($Actual, $Expected)
if ($Actual -cne $Expected) {
throw "Expected '$Expected', got '$Actual'"
}
}

function Invoke-SetupTest {
param([string]$Expected)
Set-Content -LiteralPath $env:GITHUB_ENV -Value ''
Set-Content -LiteralPath $env:GITHUB_OUTPUT -Value ''
# Use a fresh process, just like a separate action step.
& (Join-Path $PSHOME 'pwsh') -NoProfile -File $setupScript
Assert-Equal $LASTEXITCODE 0
foreach ($line in Get-Content -LiteralPath $env:GITHUB_ENV) {
if ($line -match '^([^=]+)=(.*)$') {
Set-Item -Path "Env:$($matches[1])" -Value $matches[2]
}
}
Assert-Equal $env:CLOUDSMITH_API_KEY $Expected
Assert-Equal (Get-Content (Join-Path $testRoot 'verified.log') | Select-Object -Last 1) $Expected
Assert-Equal ((Get-Content $env:GITHUB_ENV) -contains "CLOUDSMITH_API_KEY=$Expected") $true
if ($env:INPUT_EXPORT_AUTH_TOKEN -eq 'true' -or $env:INPUT_OIDC_AUTH_ONLY -eq 'true') {
Assert-Equal $env:CLOUDSMITH_USERNAME 'token'
Assert-Equal ((Get-Content $env:GITHUB_OUTPUT) -contains "oidc-token=$Expected") $true
}
}

try {
New-Item -ItemType Directory -Path "$testRoot/installer", "$testRoot/bin" | Out-Null
@'
param($Version, $InstallRoot, $OutputFile)
$binDirectory = Join-Path $env:GITHUB_ACTION_PATH 'bin'
$executable = Join-Path $binDirectory 'cloudsmith.ps1'
Set-Content -LiteralPath $OutputFile -Value @(
'version=1.21.0', 'target=test', "bin_dir=$binDirectory", "executable=$executable"
)
'@ | Set-Content -LiteralPath "$testRoot/installer/install.ps1"
@'
$global:LASTEXITCODE = 0
switch ($args -join ' ') {
'credential-helper generic' {
Add-Content "$env:GITHUB_ACTION_PATH/helper.log" 'helper'
# Match CLI precedence: an API key wins over OIDC.
$token = $env:CLOUDSMITH_API_KEY
if ([string]::IsNullOrEmpty($token)) {
Add-Content "$env:GITHUB_ACTION_PATH/exchanges.log" $env:CLOUDSMITH_SERVICE_SLUG
$token = "token-$env:CLOUDSMITH_SERVICE_SLUG"
}
@{ version = 1; username = 'token'; password = $token } | ConvertTo-Json -Compress
}
'whoami' { Add-Content "$env:GITHUB_ACTION_PATH/verified.log" $env:CLOUDSMITH_API_KEY }
default { throw "Unexpected CLI arguments: $args" }
}
'@ | Set-Content -LiteralPath "$testRoot/bin/cloudsmith.ps1"

$env:GITHUB_ACTION_PATH = $testRoot
$env:RUNNER_TEMP = $testRoot
$env:GITHUB_ENV = Join-Path $testRoot 'env'
$env:GITHUB_OUTPUT = Join-Path $testRoot 'output'
$env:GITHUB_PATH = Join-Path $testRoot 'path'
$env:GITHUB_REPOSITORY_OWNER = 'test-owner'
$env:ACTIONS_ID_TOKEN_REQUEST_URL = 'https://example.invalid/oidc'
$env:INPUT_CLI_VERSION = 'latest'
$env:INPUT_INSTALL_DIRECTORY = ''
$env:INPUT_API_KEY = ''
$env:INPUT_OIDC_NAMESPACE = 'test-org'
$env:INPUT_OIDC_SERVICE_SLUG = 'pull-only'
$env:INPUT_OIDC_AUDIENCE = ''
$env:INPUT_API_HOST = ''
$env:INPUT_API_PROXY = ''
$env:INPUT_API_SSL_VERIFY = ''
$env:INPUT_API_USER_AGENT = ''
$env:INPUT_VERIFY_AUTH = 'true'
$env:INPUT_EXPORT_AUTH_TOKEN = 'true'
$env:INPUT_OIDC_AUTH_ONLY = 'false'
$env:CLOUDSMITH_API_KEY = $null

Invoke-SetupTest 'token-pull-only'
$env:INPUT_OIDC_SERVICE_SLUG = 'push-capable'
Invoke-SetupTest 'token-push-capable'
Assert-Equal ((Get-Content "$testRoot/exchanges.log") -join ',') 'pull-only,push-capable'
Assert-Equal @(Get-Content "$testRoot/helper.log").Count 2

# Explicit API keys still win, even with OIDC inputs and an inherited token.
$env:INPUT_API_KEY = 'explicit-key'
Invoke-SetupTest 'explicit-key'
$env:INPUT_OIDC_NAMESPACE = ''
$env:INPUT_OIDC_SERVICE_SLUG = ''
$env:INPUT_API_KEY = 'api-only-key'
Invoke-SetupTest 'api-only-key'
Assert-Equal ((Get-Content "$testRoot/exchanges.log") -join ',') 'pull-only,push-capable'
Assert-Equal @(Get-Content "$testRoot/helper.log").Count 4

$env:INPUT_EXPORT_AUTH_TOKEN = 'false'
$env:INPUT_API_KEY = 'unexported-key'
Invoke-SetupTest 'unexported-key'
Assert-Equal @(Get-Content "$testRoot/helper.log").Count 4
Assert-Equal ([bool]((Get-Content $env:GITHUB_OUTPUT) -match '^oidc-token=')) $false

# The deprecated alias must also ignore the previously exported API key.
$env:INPUT_API_KEY = ''
$env:INPUT_OIDC_NAMESPACE = 'test-org'
$env:INPUT_OIDC_SERVICE_SLUG = 'alias-service'
$env:INPUT_OIDC_AUTH_ONLY = 'true'
Invoke-SetupTest 'token-alias-service'
Assert-Equal ((Get-Content "$testRoot/exchanges.log") -join ',') 'pull-only,push-capable,alias-service'
Write-Output 'PowerShell setup tests passed'
}
finally {
if (Test-Path -LiteralPath $testRoot) {
Remove-Item -LiteralPath $testRoot -Recurse -Force
}
}
90 changes: 90 additions & 0 deletions scripts/test-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -euo pipefail

setup_script="$(cd "$(dirname "$0")" && pwd)/setup.sh"
test_root="$(mktemp -d)"
trap 'rm -rf "$test_root"' EXIT
mkdir -p "$test_root/installer" "$test_root/bin"

cat > "$test_root/installer/install.sh" <<'EOF'
#!/bin/sh
while [ "$1" != "--output-file" ]; do shift; done
printf 'version=1.21.0\ntarget=test\nbin_dir=%s/bin\nexecutable=%s/bin/cloudsmith\n' \
"$GITHUB_ACTION_PATH" "$GITHUB_ACTION_PATH" > "$2"
EOF

cat > "$test_root/bin/cloudsmith" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
case "$*" in
"credential-helper generic")
echo helper >> "$GITHUB_ACTION_PATH/helper.log"
# Match CLI precedence: an API key wins over OIDC.
token="${CLOUDSMITH_API_KEY:-}"
if [[ -z "$token" ]]; then
echo "$CLOUDSMITH_SERVICE_SLUG" >> "$GITHUB_ACTION_PATH/exchanges.log"
token="token-$CLOUDSMITH_SERVICE_SLUG"
fi
printf '{"version":1,"username":"token","password":"%s"}\n' "$token"
;;
whoami) echo "${CLOUDSMITH_API_KEY:-}" >> "$GITHUB_ACTION_PATH/verified.log" ;;
*) exit 1 ;;
esac
EOF
chmod +x "$test_root/bin/cloudsmith"

export GITHUB_ACTION_PATH="$test_root" RUNNER_TEMP="$test_root"
export GITHUB_ENV="$test_root/env" GITHUB_OUTPUT="$test_root/output" GITHUB_PATH="$test_root/path"
export GITHUB_REPOSITORY_OWNER=test-owner ACTIONS_ID_TOKEN_REQUEST_URL=https://example.invalid/oidc
export INPUT_CLI_VERSION=latest INPUT_INSTALL_DIRECTORY="" INPUT_API_KEY=""
export INPUT_OIDC_NAMESPACE=test-org INPUT_OIDC_SERVICE_SLUG=pull-only INPUT_OIDC_AUDIENCE=""
export INPUT_API_HOST="" INPUT_API_PROXY="" INPUT_API_SSL_VERIFY="" INPUT_API_USER_AGENT=""
export INPUT_VERIFY_AUTH=true INPUT_EXPORT_AUTH_TOKEN=true INPUT_OIDC_AUTH_ONLY=false
unset CLOUDSMITH_API_KEY

run_setup() {
local expected="$1" line
: > "$GITHUB_ENV"
: > "$GITHUB_OUTPUT"
bash "$setup_script"
# Apply GITHUB_ENV as the runner would before the next action invocation.
while IFS= read -r line; do
export "${line?}"
done < "$GITHUB_ENV"
test "$CLOUDSMITH_API_KEY" = "$expected"
test "$(tail -n 1 "$test_root/verified.log")" = "$expected"
grep -Fx "CLOUDSMITH_API_KEY=$expected" "$GITHUB_ENV"
if [[ "$INPUT_EXPORT_AUTH_TOKEN" == true || "$INPUT_OIDC_AUTH_ONLY" == true ]]; then
test "$CLOUDSMITH_USERNAME" = token
grep -Fx "oidc-token=$expected" "$GITHUB_OUTPUT"
fi
}

run_setup token-pull-only
export INPUT_OIDC_SERVICE_SLUG=push-capable
run_setup token-push-capable
test "$(cat "$test_root/exchanges.log")" = $'pull-only\npush-capable'
test "$(wc -l < "$test_root/helper.log" | tr -d ' ')" = 2

# Explicit API keys still win, even with OIDC inputs and an inherited token.
export INPUT_API_KEY=explicit-key
run_setup explicit-key
export INPUT_OIDC_NAMESPACE="" INPUT_OIDC_SERVICE_SLUG="" INPUT_API_KEY=api-only-key
run_setup api-only-key
test "$(cat "$test_root/exchanges.log")" = $'pull-only\npush-capable'
test "$(wc -l < "$test_root/helper.log" | tr -d ' ')" = 4

export INPUT_EXPORT_AUTH_TOKEN=false INPUT_API_KEY=unexported-key
run_setup unexported-key
test "$(wc -l < "$test_root/helper.log" | tr -d ' ')" = 4
if grep -q '^oidc-token=' "$GITHUB_OUTPUT"; then
echo "Unexpected oidc-token output" >&2
exit 1
fi

# The deprecated alias must also ignore the previously exported API key.
export INPUT_API_KEY="" INPUT_OIDC_NAMESPACE=test-org INPUT_OIDC_SERVICE_SLUG=alias-service
export INPUT_OIDC_AUTH_ONLY=true
run_setup token-alias-service
test "$(cat "$test_root/exchanges.log")" = $'pull-only\npush-capable\nalias-service'
echo "Bash setup tests passed"
Loading