Skip to content

Commit 831c5ce

Browse files
feat(core): safety trinity — SKIPPER_FAIL_OPEN, SKIPPER_CACHE_TTL, SKIPPER_SYNC_ALLOW_DELETE
- SKIPPER_FAIL_OPEN (default: true): on API failure with no valid cache, run all tests instead of crashing; set to false to rethrow as before. - SKIPPER_CACHE_TTL (default: 300s): write .skipper-cache.json after every successful fetch and use it as a fallback within the TTL on API failure. - SKIPPER_SYNC_ALLOW_DELETE (default: false): in sync mode, orphaned rows are now only logged by default; set to true to allow deletion (previous behaviour). Closes #1
1 parent e7b2ab8 commit 831c5ce

5 files changed

Lines changed: 138 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [1.1.0] - 2026-03-28
11+
12+
### Added
13+
- `SKIPPER_FAIL_OPEN` env var: when the API is unreachable and no valid cache exists, run all tests instead of crashing (default: `true`). Set to `false` to restore the previous behaviour of rethrowing the exception.
14+
- `SKIPPER_CACHE_TTL` env var: after every successful fetch, skipper writes a local `.skipper-cache.json` file. On subsequent API failures the cache is used as a fallback if its age is within this TTL (default: `300` seconds).
15+
- `SKIPPER_SYNC_ALLOW_DELETE` env var: in sync mode, orphaned rows are now only logged by default. Set to `true` to allow skipper to delete them from the spreadsheet (previous behaviour).
16+
17+
## [1.0.0] - 2026-03-26
18+
19+
### Added
20+
- Initial release with support for PHPUnit (10/11/12), Pest (v2/v3), Behat (3.x), Codeception (5.x), PHPSpec (7/8), and Kahlan (5.x/6.x).
21+
- Google Sheets-based test-gating via `disabledUntil` dates.
22+
- Sync mode (`SKIPPER_MODE=sync`) to reconcile the spreadsheet with the discovered test suite.
23+
- Reference sheets support for merging test entries from multiple sheets.
24+
- Parallel test execution support via cross-process resolver cache (`SKIPPER_CACHE_FILE`, `SKIPPER_DISCOVERED_DIR`).
25+
- Three credential formats: file path, base64 string, environment variable.
26+
27+
[Unreleased]: https://github.com/get-skipper/skipper-php/compare/v1.1.0...HEAD
28+
[1.1.0]: https://github.com/get-skipper/skipper-php/compare/v1.0.0...v1.1.0
29+
[1.0.0]: https://github.com/get-skipper/skipper-php/releases/tag/v1.0.0

CONTRIBUTING.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,36 @@ chore: update google/apiclient to ^2.16
9191

9292
---
9393

94+
## Changelog
95+
96+
This project uses [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format in `CHANGELOG.md`.
97+
98+
Every pull request that changes user-facing behaviour **must** include a `CHANGELOG.md` entry under `[Unreleased]`.
99+
100+
### Categories
101+
102+
| Category | When to use |
103+
|----------|-------------|
104+
| `Added` | New features or env vars |
105+
| `Changed` | Changes to existing behaviour |
106+
| `Deprecated` | Features that will be removed in a future release |
107+
| `Removed` | Features removed in this release |
108+
| `Fixed` | Bug fixes |
109+
| `Security` | Security-related fixes |
110+
111+
### Example entry
112+
113+
```markdown
114+
## [Unreleased]
115+
116+
### Added
117+
- `SKIPPER_FAIL_OPEN` env var: run all tests instead of crashing when the API is unreachable and no cache exists (default: `true`).
118+
```
119+
120+
Releases are cut by a maintainer: the `[Unreleased]` section is renamed to the new version with its release date, and a new empty `[Unreleased]` section is added at the top.
121+
122+
---
123+
94124
## Pull requests
95125

96126
1. Fork the repository and create a branch:

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,10 @@ referenceSheets: ['SharedDisabled', 'QA']
365365
| Variable | Default | Description |
366366
|----------|---------|-------------|
367367
| `SKIPPER_MODE` | `read-only` | Set to `sync` to enable spreadsheet reconciliation |
368-
| `SKIPPER_CACHE_FILE` | _(auto)_ | Path to the resolver cache file (set by the main process) |
368+
| `SKIPPER_FAIL_OPEN` | `true` | On API failure with no valid cache, run all tests instead of crashing. Set to `false` to rethrow the exception |
369+
| `SKIPPER_CACHE_TTL` | `300` | Seconds the on-disk fallback cache (`.skipper-cache.json`) remains valid after a successful fetch |
370+
| `SKIPPER_SYNC_ALLOW_DELETE` | `false` | In sync mode, delete orphaned rows from the spreadsheet. When `false`, orphaned rows are only logged |
371+
| `SKIPPER_CACHE_FILE` | _(auto)_ | Path to the resolver cache file (set by the main process for parallel workers) |
369372
| `SKIPPER_DISCOVERED_DIR` | _(auto)_ | Directory for collecting discovered test IDs across workers |
370373
| `SKIPPER_DEBUG` | _(unset)_ | Set to any non-empty value to enable verbose logging |
371374

src/Core/Resolver/SkipperResolver.php

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,80 @@ public function __construct(
3939
/**
4040
* Fetches the spreadsheet and populates the in-memory cache.
4141
* Must be called once before isTestEnabled().
42+
*
43+
* On API failure:
44+
* - Uses the on-disk fallback cache if within SKIPPER_CACHE_TTL seconds (default: 300).
45+
* - If no valid cache exists and SKIPPER_FAIL_OPEN is not "false", runs all tests
46+
* instead of crashing (fail-open is the default).
47+
* - If SKIPPER_FAIL_OPEN=false, the exception is rethrown.
4248
*/
4349
public function initialize(): void
4450
{
45-
$result = $this->client->fetchAll();
46-
$this->cache = [];
51+
$cacheFile = '.skipper-cache.json';
52+
$ttl = (int)(getenv('SKIPPER_CACHE_TTL') ?: 300);
53+
$failOpen = getenv('SKIPPER_FAIL_OPEN') !== 'false';
54+
55+
try {
56+
$result = $this->client->fetchAll();
57+
$this->cache = [];
58+
59+
foreach ($result->entries as $entry) {
60+
$key = TestIdHelper::normalize($entry->testId);
61+
$this->cache[$key] = $entry->disabledUntil?->format(\DateTimeInterface::ATOM);
62+
}
63+
64+
file_put_contents($cacheFile, json_encode(['ts' => time(), 'rows' => $this->cache], \JSON_THROW_ON_ERROR));
65+
$this->initialized = true;
66+
} catch (\Exception $e) {
67+
$cached = $this->tryReadCache($cacheFile, $ttl);
68+
if ($cached !== null) {
69+
error_log('[skipper] API failed, using cache (' . $cached['ageSeconds'] . 's old): ' . $e->getMessage());
70+
$this->cache = $cached['rows'];
71+
$this->initialized = true;
72+
return;
73+
}
74+
if ($failOpen) {
75+
error_log('[skipper] API failed, no cache — running all tests (fail-open): ' . $e->getMessage());
76+
$this->cache = [];
77+
$this->initialized = true;
78+
return;
79+
}
80+
throw $e;
81+
}
82+
}
83+
84+
/**
85+
* Reads the on-disk fallback cache and returns rows + age if within TTL.
86+
*
87+
* @return array{rows: array<string, string|null>, ageSeconds: int}|null
88+
*/
89+
private function tryReadCache(string $cacheFile, int $ttl): ?array
90+
{
91+
if (!file_exists($cacheFile)) {
92+
return null;
93+
}
94+
95+
$raw = file_get_contents($cacheFile);
96+
if ($raw === false) {
97+
return null;
98+
}
99+
100+
try {
101+
$data = json_decode($raw, true, 512, \JSON_THROW_ON_ERROR);
102+
} catch (\JsonException) {
103+
return null;
104+
}
105+
106+
if (!isset($data['ts'], $data['rows']) || !is_int($data['ts']) || !is_array($data['rows'])) {
107+
return null;
108+
}
47109

48-
foreach ($result->entries as $entry) {
49-
$key = TestIdHelper::normalize($entry->testId);
50-
$this->cache[$key] = $entry->disabledUntil?->format(\DateTimeInterface::ATOM);
110+
$ageSeconds = time() - $data['ts'];
111+
if ($ageSeconds > $ttl) {
112+
return null;
51113
}
52114

53-
$this->initialized = true;
115+
return ['rows' => $data['rows'], 'ageSeconds' => $ageSeconds];
54116
}
55117

56118
/**

src/Core/Writer/SheetsWriter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ public function sync(array $discoveredIds): void
6666
fn ($nid) => !isset($normalizedDiscovered[$nid])
6767
);
6868

69+
$allowDeletes = getenv('SKIPPER_SYNC_ALLOW_DELETE') === 'true';
70+
71+
if (!$allowDeletes && !empty($toRemoveNormalized)) {
72+
Logger::log('[skipper] ' . count($toRemoveNormalized) . ' orphaned row(s) found. Set SKIPPER_SYNC_ALLOW_DELETE=true to prune them.');
73+
$toRemoveNormalized = [];
74+
}
75+
6976
if (empty($toAdd) && empty($toRemoveNormalized)) {
7077
Logger::log('[skipper] Spreadsheet is already up to date.');
7178
return;

0 commit comments

Comments
 (0)