Skip to content
Merged
158 changes: 158 additions & 0 deletions .cursor/rules/github-workflow.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,161 @@ For reusable workflows (workflow_call), use descriptive names that indicate thei
- Ensure names remain meaningful when viewed in GitHub's status check UI
- Test names in the GitHub PR interface before committing changes
- For lint workflows, use simple "Lint" job name since the tool is already specified in the workflow name

---

## GitHub Actions Concurrency Strategy

### Overview

This document outlines the concurrency configuration strategy for all GitHub Actions workflows in the Sentry Cocoa repository. The strategy optimizes CI resource usage while ensuring critical runs (like main branch pushes) are never interrupted.

### Core Principles

#### 1. Resource Optimization
- **Cancel outdated PR runs** - When new commits are pushed to a PR, cancel the previous workflow run since only the latest commit matters for merge decisions
- **Protect critical runs** - Never cancel workflows running on main branch, release branches, or scheduled runs as these are essential for maintaining baseline quality and release integrity
- **Per-branch grouping** - Use `github.ref` for consistent concurrency grouping across all branch types

#### 2. Consistent Patterns
All workflows follow standardized concurrency patterns based on their trigger types and criticality.

### Concurrency Patterns

#### Pattern 1: Conditional Cancellation (Most Common)
**Used by:** Most workflows that run on both main/release branches AND pull requests

```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
```

**Behavior:**
- ✅ Cancels in-progress runs when new commits are pushed to PRs
- ✅ Never cancels runs on main branch pushes
- ✅ Never cancels runs on release branch pushes
- ✅ Never cancels scheduled runs
- ✅ Never cancels manual workflow_dispatch runs

**Examples:** `test.yml`, `build.yml`, `benchmarking.yml`, `ui-tests.yml`, all lint workflows

#### Pattern 2: Always Cancel (PR-Only Workflows)
**Used by:** Workflows that ONLY run on pull requests

```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
```

**Behavior:**
- ✅ Always cancels in-progress runs (safe since they only run on PRs)
- ✅ Provides immediate feedback on latest changes

**Examples:** `danger.yml`, `api-stability.yml`, `changes-in-high-risk-code.yml`

#### Pattern 3: Fixed Group Name (Special Cases)
**Used by:** Utility workflows with specific requirements

```yaml
concurrency:
group: "auto-update-tools"
cancel-in-progress: true
```

**Example:** `auto-update-tools.yml` (uses fixed group name for global coordination)

### Implementation Details

#### Group Naming Convention
- **Standard:** `${{ github.workflow }}-${{ github.ref }}`
- **Benefits:**
- Unique per workflow and branch/PR
- Consistent across all workflow types
- Works with main, release, and feature branches
- Handles PRs and direct pushes uniformly

#### Why `github.ref` Instead of `github.head_ref || github.run_id`?
- **Simpler logic** - No conditional expressions needed
- **Consistent behavior** - Same pattern works for all trigger types
- **Per-branch grouping** - Natural grouping by branch without special cases
- **Better maintainability** - Single pattern to understand and maintain

#### Cancellation Logic Evolution
**Before:**
```yaml
cancel-in-progress: ${{ !(github.event_name == 'push' && github.ref == 'refs/heads/main') && github.event_name != 'schedule' }}
```

**After:**
```yaml
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
```

**Why simplified:**
- ✅ Much more readable and maintainable
- ✅ Functionally identical behavior
- ✅ Clear intent: "only cancel on pull requests"
- ✅ Less prone to errors

### Workflow-Specific Configurations

#### High-Resource Workflows
**Examples:** `benchmarking.yml`, `ui-tests.yml`
- Use conditional cancellation to protect expensive main branch runs
- Include detailed comments explaining resource considerations
- May include special cleanup steps (e.g., SauceLabs job cancellation)

#### Fast Validation Workflows
**Examples:** All lint workflows, `danger.yml`
- Use appropriate cancellation strategy based on trigger scope
- Focus on providing quick feedback on latest changes

#### Critical Infrastructure Workflows
**Examples:** `test.yml`, `build.yml`, `release.yml`
- Never cancel on main/release branches to maintain quality gates
- Ensure complete validation of production-bound code

### Documentation Requirements

Each workflow's concurrency block must include comments explaining:

1. **Purpose** - Why concurrency control is needed for this workflow
2. **Resource considerations** - Any expensive operations (SauceLabs, device time, etc.)
3. **Branch protection logic** - Why main/release branches need complete runs
4. **User experience** - How the configuration improves feedback timing

#### Example Documentation:
```yaml
# Concurrency configuration:
# - We use workflow-specific concurrency groups to prevent multiple benchmark runs on the same code,
# as benchmarks are extremely resource-intensive and require dedicated device time on SauceLabs.
# - For pull requests, we cancel in-progress runs when new commits are pushed to avoid wasting
# expensive external testing resources and provide timely performance feedback.
# - For main branch pushes, we never cancel benchmarks to ensure we have complete performance
# baselines for every main branch commit, which are critical for performance regression detection.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
```

### Maintenance Guidelines

#### When Adding New Workflows
1. **Identify trigger scope** - Does it run on main/release branches?
2. **Choose appropriate pattern** - Conditional vs always cancel
3. **Add documentation** - Explain the resource and timing considerations
4. **Follow naming convention** - Use standard group naming pattern

#### When Modifying Existing Workflows
1. **Preserve protection** - Don't break main/release branch safeguards
2. **Update documentation** - Keep comments accurate and helpful
3. **Test edge cases** - Verify behavior with scheduled/manual triggers
4. **Consider resource impact** - Evaluate cost of additional runs

#### Red Flags to Avoid
- ❌ Never use `cancel-in-progress: true` on workflows that run on main/release branches
- ❌ Don't create complex conditional logic when simple patterns work
- ❌ Avoid custom group names unless absolutely necessary
- ❌ Don't skip documentation - future maintainers need context
9 changes: 9 additions & 0 deletions .github/workflows/api-stability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ on:
- "sdk_api.json"
- "sdk_api_v9.json"

# Concurrency configuration:
# - We use workflow-specific concurrency groups to prevent multiple API stability checks on the same code,
# as these analyze public API changes and generate detailed breaking change reports.
# - We always cancel in-progress runs since this workflow only runs on pull requests, and only
# the latest commit's API changes matter for determining if the PR introduces breaking changes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
api-stability:
name: Check API Stability (${{ matrix.version }})
Expand Down
118 changes: 113 additions & 5 deletions .github/workflows/benchmarking.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ on:
- ".github/workflows/build-xcframework-variant-slices.yml"
- ".github/workflows/assemble-xcframework-variant.yml"

# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
# Concurrency configuration:
# - We use workflow-specific concurrency groups to prevent multiple benchmark runs on the same code,
# as benchmarks are extremely resource-intensive and require dedicated device time on SauceLabs.
# - For pull requests, we cancel in-progress runs when new commits are pushed to avoid wasting
# expensive external testing resources and provide timely performance feedback.
# - For main branch pushes, we never cancel benchmarks to ensure we have complete performance
# baselines for every main branch commit, which are critical for performance regression detection.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build-benchmark-test-target:
Expand Down Expand Up @@ -171,8 +177,110 @@ jobs:
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
run: |
echo "::warning SauceLabs benchmark tests need to be retried"
saucectl run \
set -o pipefail && saucectl run \
--select-suite "${{matrix.suite}}" \
--config .sauce/benchmarking-config.yml \
--tags benchmark \
--verbose
--verbose \
2>&1 | tee retry-output.log

- name: Force Cancel SauceLabs Job on Workflow Cancellation
if: ${{ cancelled() }}
uses: actions/github-script@v7
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
with:
script: |
const fs = require('fs');

console.log("::warning Workflow was cancelled, attempting to cancel SauceLabs jobs");

// SauceLabs API configuration
const sauceUsername = process.env.SAUCE_USERNAME;
const sauceAccessKey = process.env.SAUCE_ACCESS_KEY;
const apiBaseUrl = 'https://api.us-west-1.saucelabs.com';

if (!sauceUsername || !sauceAccessKey) {
console.log("::error SauceLabs credentials not found, cannot cancel jobs");
return;
}

// Function to extract job IDs from log files
const extractJobIdsFromLog = (logFile) => {
if (!fs.existsSync(logFile)) {
console.log(`Log file ${logFile} not found, skipping...`);
return [];
}

console.log(`Checking ${logFile} for SauceLabs test IDs...`);
const logContent = fs.readFileSync(logFile, 'utf8');

// Extract all SauceLabs test URLs and get job IDs
const urlRegex = /https:\/\/app\.saucelabs\.com\/tests\/([^\s]+)/g;
const jobIds = [];
let match;

while ((match = urlRegex.exec(logContent)) !== null) {
const jobId = match[1];
if (jobId) {
jobIds.push(jobId);
console.log(`Found SauceLabs job ID: ${jobId}`);
}
}

return jobIds;
};

// Function to cancel a SauceLabs job via REST API
// Reference: https://docs.saucelabs.com/dev/api/jobs/#stop-a-job
const cancelSauceLabsJob = async (jobId) => {
try {
console.log(`Attempting to cancel SauceLabs job: ${jobId}`);

// Create basic auth header
const auth = Buffer.from(`${sauceUsername}:${sauceAccessKey}`).toString('base64');

// Make PUT request to stop the job
const response = await fetch(`${apiBaseUrl}/rest/v1/${sauceUsername}/jobs/${jobId}/stop`, {
method: 'PUT',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
}
});

if (response.ok) {
console.log(`Successfully cancelled SauceLabs job: ${jobId}`);
} else if (response.status === 404) {
console.log(`Job ${jobId} not found (may already be finished)`);
} else {
const errorText = await response.text();
console.log(`Failed to cancel job ${jobId}: HTTP ${response.status} - ${errorText}`);
}
} catch (error) {
console.log(`Error cancelling job ${jobId}: ${error.message}`);
}
};

// Extract job IDs from both possible log files
const jobIds = [
...extractJobIdsFromLog('output.log'),
...extractJobIdsFromLog('retry-output.log')
];

// Remove duplicates
const uniqueJobIds = [...new Set(jobIds)];

if (uniqueJobIds.length === 0) {
console.log("No SauceLabs job IDs found in log files");
return;
}

// Cancel all found jobs
console.log(`Found ${uniqueJobIds.length} unique job(s) to cancel`);
for (const jobId of uniqueJobIds) {
await cancelSauceLabsJob(jobId);
}

console.log("SauceLabs job cancellation attempts completed");
12 changes: 9 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ on:
- "Makefile" # Make commands used for CI build setup
- "Brewfile*" # Dependency installation affects build environment

# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
# Concurrency configuration:
# - We use workflow-specific concurrency groups to prevent multiple build runs of the same code,
# which would waste CI resources without providing additional value.
# - For pull requests, we cancel in-progress builds when new commits are pushed since only the
# latest commit's build results matter for merge decisions.
# - For main branch pushes, we never cancel builds to ensure all release and sample builds complete,
# as broken builds on main could block releases and affect downstream consumers.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
# We had issues that the release build was broken on main.
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/changes-in-high-risk-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ name: Changes In High Risk Code
on:
pull_request:

# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
# Concurrency configuration:
# - We use workflow-specific concurrency groups to prevent multiple high-risk code analysis runs,
# as these detect changes to critical code paths and trigger additional validation workflows.
# - We always cancel in-progress runs since this workflow only runs on pull requests, and only
# the latest commit's high-risk code changes matter for determining which additional tests to run.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand Down
12 changes: 9 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ on:
schedule:
- cron: "40 4 * * 6" # Weekly scheduled run to catch issues regardless of changes

# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
# Concurrency configuration:
# - We use workflow-specific concurrency groups to prevent multiple CodeQL security analysis runs,
# as these are comprehensive security scans that shouldn't run simultaneously.
# - For pull requests, we cancel in-progress runs when new commits are pushed since only the
# latest security analysis results matter for identifying potential vulnerabilities.
# - For main branch pushes and scheduled runs, we never cancel security analysis to ensure
# complete security validation and maintain our security baseline with weekly scheduled scans.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
analyze:
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/danger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ on:
pull_request:
types: [opened, synchronize, reopened, edited, ready_for_review]

# Concurrency configuration:
# - We use workflow-specific concurrency groups to prevent multiple Danger runs on the same PR,
# as Danger performs PR validation and comment generation that should be based on the latest code.
# - We always cancel in-progress runs since this workflow only runs on pull requests, and only
# the latest commit's validation results matter for providing accurate PR feedback and reviews.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
danger:
name: Danger
Expand Down
Loading
Loading