|
| 1 | +# Contract: Setup-Test Composite Action |
| 2 | + |
| 3 | +**File**: `.github/actions/setup-test/action.yml` |
| 4 | +**Type**: GitHub Actions Composite Action |
| 5 | +**Purpose**: Execute BeforeAll.ps1 or AfterAll.ps1 test setup/teardown scripts based on mode parameter |
| 6 | + |
| 7 | +## Action Definition Contract |
| 8 | + |
| 9 | +This is the contract specification for the composite action. The actual implementation will be in `.github/actions/setup-test/action.yml`. |
| 10 | + |
| 11 | +```yaml |
| 12 | +name: 'Setup-Test' |
| 13 | +description: 'Execute BeforeAll or AfterAll test setup/teardown scripts based on mode parameter' |
| 14 | +author: 'PSModule' |
| 15 | + |
| 16 | +inputs: |
| 17 | + mode: |
| 18 | + description: 'Execution mode: "before" (BeforeAll.ps1) or "after" (AfterAll.ps1)' |
| 19 | + required: true |
| 20 | + |
| 21 | + Debug: |
| 22 | + description: 'Enable debug output' |
| 23 | + required: false |
| 24 | + default: 'false' |
| 25 | + |
| 26 | + Verbose: |
| 27 | + description: 'Enable verbose output' |
| 28 | + required: false |
| 29 | + default: 'false' |
| 30 | + |
| 31 | + Prerelease: |
| 32 | + description: 'Use prerelease version of GitHub module' |
| 33 | + required: false |
| 34 | + default: 'false' |
| 35 | + |
| 36 | + Version: |
| 37 | + description: 'Exact version of GitHub module to install' |
| 38 | + required: false |
| 39 | + default: '' |
| 40 | + |
| 41 | + WorkingDirectory: |
| 42 | + description: 'Working directory where script will run from' |
| 43 | + required: false |
| 44 | + default: '.' |
| 45 | + |
| 46 | +runs: |
| 47 | + using: 'composite' |
| 48 | + steps: |
| 49 | + - name: Install-PSModuleHelpers |
| 50 | + uses: PSModule/Install-PSModuleHelpers@v1 |
| 51 | + |
| 52 | + - name: Execute Setup/Teardown Script |
| 53 | + uses: PSModule/GitHub-Script@v1 |
| 54 | + with: |
| 55 | + Name: 'Setup-Test-${{ inputs.mode }}' |
| 56 | + ShowInfo: false |
| 57 | + ShowOutput: true |
| 58 | + Debug: ${{ inputs.Debug }} |
| 59 | + Verbose: ${{ inputs.Verbose }} |
| 60 | + Prerelease: ${{ inputs.Prerelease }} |
| 61 | + Version: ${{ inputs.Version }} |
| 62 | + WorkingDirectory: ${{ inputs.WorkingDirectory }} |
| 63 | + Script: | |
| 64 | + # PowerShell script implementation |
| 65 | + # See script-implementation.md for detailed script logic |
| 66 | +``` |
| 67 | +
|
| 68 | +## Inputs Contract |
| 69 | +
|
| 70 | +### Required Inputs |
| 71 | +
|
| 72 | +| Input | Type | Validation | Example | |
| 73 | +|-------|------|------------|---------| |
| 74 | +| mode | string | Must be "before" or "after" | "before" | |
| 75 | +
|
| 76 | +### Optional Inputs |
| 77 | +
|
| 78 | +| Input | Type | Default | Description | |
| 79 | +|-------|------|---------|-------------| |
| 80 | +| Debug | boolean | false | Enable debug output | |
| 81 | +| Verbose | boolean | false | Enable verbose output | |
| 82 | +| Prerelease | boolean | false | Use prerelease GitHub module | |
| 83 | +| Version | string | '' | Exact GitHub module version | |
| 84 | +| WorkingDirectory | string | '.' | Script working directory | |
| 85 | +
|
| 86 | +## Environment Variables Contract |
| 87 | +
|
| 88 | +The action expects these environment variables to be set by the caller (passed as secrets): |
| 89 | +
|
| 90 | +| Variable | Purpose | Required | |
| 91 | +|----------|---------|----------| |
| 92 | +| TEST_APP_ENT_CLIENT_ID | Enterprise GitHub App client ID | Optional | |
| 93 | +| TEST_APP_ENT_PRIVATE_KEY | Enterprise GitHub App private key | Optional | |
| 94 | +| TEST_APP_ORG_CLIENT_ID | Organization GitHub App client ID | Optional | |
| 95 | +| TEST_APP_ORG_PRIVATE_KEY | Organization GitHub App private key | Optional | |
| 96 | +| TEST_USER_ORG_FG_PAT | Fine-grained PAT with org access | Optional | |
| 97 | +| TEST_USER_USER_FG_PAT | Fine-grained PAT with user access | Optional | |
| 98 | +| TEST_USER_PAT | Classic PAT | Optional | |
| 99 | +| GITHUB_TOKEN | GitHub Actions token | Optional | |
| 100 | +
|
| 101 | +**Note**: All environment variables are optional. Scripts may require specific variables based on their test requirements. |
| 102 | +
|
| 103 | +## Outputs Contract |
| 104 | +
|
| 105 | +**No outputs**: This action communicates results via exit codes and console output. |
| 106 | +
|
| 107 | +- **Success**: Exit code 0 (action step shows green checkmark) |
| 108 | +- **Failure**: Exit code non-zero (action step shows red X) - only for mode="before" with script errors |
| 109 | +
|
| 110 | +## Behavior Contract |
| 111 | +
|
| 112 | +### Mode: "before" |
| 113 | +
|
| 114 | +**Purpose**: Execute BeforeAll.ps1 setup script before tests |
| 115 | +
|
| 116 | +**Execution Flow**: |
| 117 | +1. Check if tests directory exists |
| 118 | + - Not found → Exit 0 with message "No tests directory found - exiting successfully" |
| 119 | +2. Check if tests/BeforeAll.ps1 exists |
| 120 | + - Not found → Exit 0 with message "No BeforeAll.ps1 script found - exiting successfully" |
| 121 | +3. Execute tests/BeforeAll.ps1 |
| 122 | + - Change to tests directory |
| 123 | + - Run script with environment variables |
| 124 | + - On success → Exit 0 with message "BeforeAll script completed successfully: [path]" |
| 125 | + - On failure → Exit non-zero with error "BeforeAll script failed: [path] - [error]" |
| 126 | + - Always restore previous directory (via finally block) |
| 127 | +
|
| 128 | +**Error Handling**: Script failures cause job failure (errors propagated) |
| 129 | +
|
| 130 | +### Mode: "after" |
| 131 | +
|
| 132 | +**Purpose**: Execute AfterAll.ps1 teardown script after tests |
| 133 | +
|
| 134 | +**Execution Flow**: |
| 135 | +1. Check if tests directory exists |
| 136 | + - Not found → Exit 0 with message "No tests directory found - exiting successfully" |
| 137 | +2. Check if tests/AfterAll.ps1 exists |
| 138 | + - Not found → Exit 0 with message "No AfterAll.ps1 script found - exiting successfully" |
| 139 | +3. Execute tests/AfterAll.ps1 |
| 140 | + - Change to tests directory |
| 141 | + - Run script with environment variables |
| 142 | + - On success → Exit 0 with message "AfterAll script completed successfully: [path]" |
| 143 | + - On failure → Exit 0 with warning "AfterAll script failed: [path] - [error]" |
| 144 | + - Always restore previous directory (via finally block) |
| 145 | +
|
| 146 | +**Error Handling**: Script failures logged as warnings but don't cause job failure (exit 0) |
| 147 | +
|
| 148 | +## Output Format Contract |
| 149 | +
|
| 150 | +All output wrapped in LogGroup with title based on mode: |
| 151 | +
|
| 152 | +**Mode "before"**: `LogGroup "Running BeforeAll Setup Scripts" { ... }` |
| 153 | + |
| 154 | +**Mode "after"**: `LogGroup "Running AfterAll Teardown Scripts" { ... }` |
| 155 | + |
| 156 | +### Standard Output Messages |
| 157 | + |
| 158 | +| Scenario | Output Message | Level | |
| 159 | +|----------|---------------|-------| |
| 160 | +| No tests directory | "No tests directory found - exiting successfully" | Info | |
| 161 | +| Tests directory found | "Tests found at [$testsPath]" | Info | |
| 162 | +| Script not found (before) | "No BeforeAll.ps1 script found - exiting successfully" | Info | |
| 163 | +| Script not found (after) | "No AfterAll.ps1 script found - exiting successfully" | Info | |
| 164 | +| Script execution start | "Running [BeforeAll\|AfterAll] setup/teardown script: $scriptPath" | Info | |
| 165 | +| Script success | "[BeforeAll\|AfterAll] script completed successfully: $scriptPath" | Info | |
| 166 | +| Script failure (before) | "BeforeAll script failed: $scriptPath - $_" | Error | |
| 167 | +| Script failure (after) | "AfterAll script failed: $scriptPath - $_" | Warning | |
| 168 | + |
| 169 | +## Caller Integration Contract |
| 170 | + |
| 171 | +### Example: BeforeAll-ModuleLocal Job |
| 172 | + |
| 173 | +```yaml |
| 174 | +BeforeAll-ModuleLocal: |
| 175 | + name: BeforeAll-ModuleLocal |
| 176 | + runs-on: ubuntu-latest |
| 177 | + steps: |
| 178 | + - name: Checkout Code |
| 179 | + uses: actions/checkout@v5 |
| 180 | +
|
| 181 | + - name: Run BeforeAll Setup |
| 182 | + uses: ./.github/actions/setup-test |
| 183 | + env: |
| 184 | + TEST_APP_ENT_CLIENT_ID: ${{ secrets.TEST_APP_ENT_CLIENT_ID }} |
| 185 | + TEST_APP_ENT_PRIVATE_KEY: ${{ secrets.TEST_APP_ENT_PRIVATE_KEY }} |
| 186 | + TEST_APP_ORG_CLIENT_ID: ${{ secrets.TEST_APP_ORG_CLIENT_ID }} |
| 187 | + TEST_APP_ORG_PRIVATE_KEY: ${{ secrets.TEST_APP_ORG_PRIVATE_KEY }} |
| 188 | + TEST_USER_ORG_FG_PAT: ${{ secrets.TEST_USER_ORG_FG_PAT }} |
| 189 | + TEST_USER_USER_FG_PAT: ${{ secrets.TEST_USER_USER_FG_PAT }} |
| 190 | + TEST_USER_PAT: ${{ secrets.TEST_USER_PAT }} |
| 191 | + GITHUB_TOKEN: ${{ github.token }} |
| 192 | + with: |
| 193 | + mode: before |
| 194 | + Debug: ${{ inputs.Debug }} |
| 195 | + Verbose: ${{ inputs.Verbose }} |
| 196 | + Prerelease: ${{ inputs.Prerelease }} |
| 197 | + Version: ${{ inputs.Version }} |
| 198 | + WorkingDirectory: ${{ inputs.WorkingDirectory }} |
| 199 | +``` |
| 200 | + |
| 201 | +### Example: AfterAll-ModuleLocal Job |
| 202 | + |
| 203 | +```yaml |
| 204 | +AfterAll-ModuleLocal: |
| 205 | + name: AfterAll-ModuleLocal |
| 206 | + runs-on: ubuntu-latest |
| 207 | + needs: |
| 208 | + - Test-ModuleLocal |
| 209 | + if: always() |
| 210 | + steps: |
| 211 | + - name: Checkout Code |
| 212 | + uses: actions/checkout@v5 |
| 213 | +
|
| 214 | + - name: Run AfterAll Teardown |
| 215 | + if: always() |
| 216 | + uses: ./.github/actions/setup-test |
| 217 | + env: |
| 218 | + TEST_APP_ENT_CLIENT_ID: ${{ secrets.TEST_APP_ENT_CLIENT_ID }} |
| 219 | + TEST_APP_ENT_PRIVATE_KEY: ${{ secrets.TEST_APP_ENT_PRIVATE_KEY }} |
| 220 | + TEST_APP_ORG_CLIENT_ID: ${{ secrets.TEST_APP_ORG_CLIENT_ID }} |
| 221 | + TEST_APP_ORG_PRIVATE_KEY: ${{ secrets.TEST_APP_ORG_PRIVATE_KEY }} |
| 222 | + TEST_USER_ORG_FG_PAT: ${{ secrets.TEST_USER_ORG_FG_PAT }} |
| 223 | + TEST_USER_USER_FG_PAT: ${{ secrets.TEST_USER_USER_FG_PAT }} |
| 224 | + TEST_USER_PAT: ${{ secrets.TEST_USER_PAT }} |
| 225 | + GITHUB_TOKEN: ${{ github.token }} |
| 226 | + with: |
| 227 | + mode: after |
| 228 | + Debug: ${{ inputs.Debug }} |
| 229 | + Verbose: ${{ inputs.Verbose }} |
| 230 | + Prerelease: ${{ inputs.Prerelease }} |
| 231 | + Version: ${{ inputs.Version }} |
| 232 | + WorkingDirectory: ${{ inputs.WorkingDirectory }} |
| 233 | +``` |
| 234 | + |
| 235 | +**Required Caller Setup**: |
| 236 | +1. Repository must be checked out (actions/checkout@v5) |
| 237 | +2. All required secrets must be passed as environment variables |
| 238 | +3. AfterAll-ModuleLocal job must have `if: always()` to ensure cleanup runs |
| 239 | +4. AfterAll-ModuleLocal step should also have `if: always()` for resilience |
| 240 | + |
| 241 | +## Validation Contract |
| 242 | + |
| 243 | +### Pre-Execution Validation |
| 244 | + |
| 245 | +1. **mode parameter**: Action script will validate mode is "before" or "after" |
| 246 | + - Invalid mode → Error with message about valid values |
| 247 | +2. **Repository checkout**: Caller responsibility to run actions/checkout@v5 |
| 248 | +3. **Working directory**: GitHub-Script handles WorkingDirectory validation |
| 249 | + |
| 250 | +### Runtime Validation |
| 251 | + |
| 252 | +1. **tests directory**: Action validates existence via Resolve-Path |
| 253 | +2. **Script file**: Action validates existence via Test-Path |
| 254 | +3. **Script execution**: Action wraps in try/catch for error handling |
| 255 | + |
| 256 | +### Post-Execution Validation |
| 257 | + |
| 258 | +1. **Working directory restore**: Guaranteed via try/finally block |
| 259 | +2. **Exit code**: 0 for success, non-zero for failure (mode="before" only) |
| 260 | +3. **Output completeness**: All status messages logged to console |
| 261 | + |
| 262 | +## Dependencies Contract |
| 263 | + |
| 264 | +### Action Dependencies |
| 265 | + |
| 266 | +1. **PSModule/Install-PSModuleHelpers@v1** |
| 267 | + - Must run before GitHub-Script step |
| 268 | + - Provides PSModule helper functions |
| 269 | + - No inputs required |
| 270 | + |
| 271 | +2. **PSModule/GitHub-Script@v1** |
| 272 | + - Executes PowerShell script |
| 273 | + - Provides LogGroup helper |
| 274 | + - Receives all action inputs and environment variables |
| 275 | + |
| 276 | +### Script Dependencies |
| 277 | + |
| 278 | +Scripts (BeforeAll.ps1, AfterAll.ps1) can assume: |
| 279 | +1. PowerShell 7.4+ environment |
| 280 | +2. All environment variables are set |
| 281 | +3. Execution directory is tests/ |
| 282 | +4. PSModule helpers are available |
| 283 | +5. ubuntu-latest runner environment |
| 284 | + |
| 285 | +## Error Handling Contract |
| 286 | + |
| 287 | +### Mode "before" Error Handling |
| 288 | + |
| 289 | +| Error Scenario | Handling | Exit Code | Job Status | |
| 290 | +|----------------|----------|-----------|------------| |
| 291 | +| Invalid mode | Error message + exit | Non-zero | Failed | |
| 292 | +| No tests directory | Info message + exit | 0 | Success | |
| 293 | +| No BeforeAll.ps1 | Info message + exit | 0 | Success | |
| 294 | +| Script execution error | Error message + throw | Non-zero | Failed | |
| 295 | +| Script execution success | Info message + exit | 0 | Success | |
| 296 | + |
| 297 | +### Mode "after" Error Handling |
| 298 | + |
| 299 | +| Error Scenario | Handling | Exit Code | Job Status | |
| 300 | +|----------------|----------|-----------|------------| |
| 301 | +| Invalid mode | Error message + exit | Non-zero | Failed | |
| 302 | +| No tests directory | Info message + exit | 0 | Success | |
| 303 | +| No AfterAll.ps1 | Info message + exit | 0 | Success | |
| 304 | +| Script execution error | Warning message + exit | 0 | Success | |
| 305 | +| Script execution success | Info message + exit | 0 | Success | |
| 306 | + |
| 307 | +### Error Messages Contract |
| 308 | + |
| 309 | +**Invalid Mode**: |
| 310 | +``` |
| 311 | +Invalid mode '$mode'. Mode must be 'before' or 'after'. |
| 312 | +``` |
| 313 | +
|
| 314 | +**Script Execution Error (before)**: |
| 315 | +``` |
| 316 | +BeforeAll script failed: [absolute-path] - [error-message] |
| 317 | +``` |
| 318 | +
|
| 319 | +**Script Execution Error (after)**: |
| 320 | +``` |
| 321 | +WARNING: AfterAll script failed: [absolute-path] - [error-message] |
| 322 | +``` |
| 323 | +
|
| 324 | +## Performance Contract |
| 325 | +
|
| 326 | +| Metric | Target | Notes | |
| 327 | +|--------|--------|-------| |
| 328 | +| Action invocation overhead | <2 seconds | Install-PSModuleHelpers + GitHub-Script startup | |
| 329 | +| Script discovery time | <100ms | Single Resolve-Path + Test-Path | |
| 330 | +| Total execution time | Script runtime + overhead | Depends on script complexity | |
| 331 | +
|
| 332 | +## Security Contract |
| 333 | +
|
| 334 | +1. **Secrets**: Passed as environment variables, not logged |
| 335 | +2. **Script execution**: Runs in isolated job context on ubuntu-latest |
| 336 | +3. **Working directory**: Restored via finally block (no directory escape) |
| 337 | +4. **Output sanitization**: Caller responsibility to avoid logging secrets in scripts |
| 338 | +
|
| 339 | +--- |
| 340 | +**Contract Complete**: Ready for test implementation and integration. |
0 commit comments