ops: add CI/CD pipeline, a/b rolling deploy, Gitea Actions workflow
Deploy to Production / deploy (push) Failing after 10s
Deploy to Production / deploy (push) Failing after 10s
- .gitea/workflows/deploy.yml — push-to-main triggers rolling deploy - scripts/deploy-bluegreen.sh — a-stack then b-stack restart; Maven runs in Docker (no JDK needed on runner host); Caddy reload at end - scripts/deploy-all.ps1 — emergency manual deploy from dev machine - infra/docker-compose.yml — a/b pairs per service; wget health checks; Gitea service; Prometheus/Grafana/DB ports restricted to localhost - infra/Caddyfile — dual upstreams with health-based routing - infra/Dockerfile.* — one per service - infra/prometheus.yml + grafana provisioning Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
Feature: Device owner anonymity guarantee
|
||||
As an IoT device owner
|
||||
I must be able to use the full transition discovery process
|
||||
Without APIX storing or inferring any artefact that identifies me or my device
|
||||
|
||||
Background:
|
||||
Given a deprecated service "SmartHub Cloud" with locked set to false
|
||||
And at least one replacement candidate registered
|
||||
|
||||
Scenario: Status polling requires no authentication
|
||||
When GET /services/{smartHubCloudId} is called with no Authorization header
|
||||
Then the response is HTTP 200
|
||||
And locked and sunset_date are present in the response body
|
||||
|
||||
Scenario: Replacement discovery requires no authentication
|
||||
When GET /services/{smartHubCloudId}/replacements is called with no Authorization header
|
||||
Then the response is HTTP 200
|
||||
And the full replacement list is returned
|
||||
|
||||
Scenario: No session state is created during polling
|
||||
When GET /services/{smartHubCloudId}/replacements is called twice with no shared headers
|
||||
Then both responses are identical in content
|
||||
And neither response contains a Set-Cookie header
|
||||
And neither response contains a session reference
|
||||
|
||||
Scenario: Response contains no client-tracking artefacts
|
||||
When GET /services/{smartHubCloudId}/replacements is called
|
||||
Then the response headers contain no Set-Cookie
|
||||
And the response body contains no field that echoes client request details
|
||||
And the response body contains no correlation ID tied to the caller
|
||||
|
||||
Scenario: Polling endpoint is covered by the public cache
|
||||
When GET /services/{smartHubCloudId}/replacements is called twice within the cache TTL
|
||||
Then the second response is served from cache
|
||||
And the cache key does not incorporate any client-identifying header
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
Feature: Service decommissioning and historical record preservation
|
||||
As a template owner
|
||||
I want to decommission a service after its sunset date
|
||||
And as a device owner I must be able to access the historical record indefinitely
|
||||
|
||||
Background:
|
||||
Given a registered service "SmartHub Cloud" with endpoint "https://api.smarthub.example"
|
||||
And the service has capability "device.telemetry"
|
||||
And the service has locked set to true
|
||||
And the service is in stage "DEPRECATED" with a sunset_date set
|
||||
And the service has locked set to false
|
||||
And a registered service "OpenHub" with endpoint "https://api.openhub.example"
|
||||
And "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
And the sunset_date of "SmartHub Cloud" has passed
|
||||
|
||||
Scenario: Template owner decommissions the service
|
||||
When the template owner sets service_stage to "DECOMMISSIONED"
|
||||
Then the service does not appear in any capability search results
|
||||
And GET /services/{id} returns HTTP 200 with the complete historical record
|
||||
And the response body contains service_stage "DECOMMISSIONED"
|
||||
And a version history entry of type "STAGE_CHANGED" exists for the service
|
||||
|
||||
Scenario: Decommissioned service with unreleased lock auto-releases for replacement discovery
|
||||
Given a deprecated service "NeverReleased" that reached DECOMMISSIONED without setting locked=false
|
||||
When GET /services/{neverReleasedId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And replacement candidates are returned regardless of the stored locked value
|
||||
|
||||
Scenario: Historical record survives indefinitely
|
||||
Given a decommissioned service
|
||||
When GET /services/{id} is called
|
||||
Then the response is HTTP 200
|
||||
And the full BSM payload is present in the response
|
||||
And all version history entries are accessible via GET /services/{id}/history
|
||||
|
||||
Scenario: Cannot decommission a service before its sunset date
|
||||
Given a deprecated service "FutureCloud" with a sunset_date 30 days from now
|
||||
When the template owner attempts to set service_stage to "DECOMMISSIONED"
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "sunset_at has not passed"
|
||||
|
||||
Scenario: Full transition timeline is visible in version history
|
||||
Given "SmartHub Cloud" has completed the full lifecycle
|
||||
When GET /services/{id}/history is called
|
||||
Then the history contains an entry of type "REGISTERED"
|
||||
And the history contains an entry of type "SUNSET_DECLARED"
|
||||
And the history contains an entry of type "LOCK_RELEASED"
|
||||
And the history contains an entry of type "REPLACEMENT_DECLARED"
|
||||
And the history contains an entry of type "STAGE_CHANGED" with new value "DECOMMISSIONED"
|
||||
And all entries are ordered chronologically ascending
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
Feature: Replacement provider declares compatibility
|
||||
As a replacement service provider
|
||||
I want to declare that my service covers a deprecated template
|
||||
So that IoT device owners can discover me as a migration target
|
||||
|
||||
Background:
|
||||
Given a deprecated service "SmartHub Cloud" with locked set to false
|
||||
And a registered service "OpenHub" in stage "PRODUCTION" with O-level "IDENTITY_VERIFIED"
|
||||
|
||||
Scenario: Replacement provider declares compatibility with a deprecated service
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "SmartHub Cloud"
|
||||
Then GET /services/{smartHubCloudId}/replacements includes "OpenHub"
|
||||
And a version history entry of type "REPLACEMENT_DECLARED" exists for "SmartHub Cloud"
|
||||
And the service_replacements table contains the declared pair
|
||||
|
||||
Scenario: Declaration against a non-deprecated service is rejected
|
||||
Given a service "ActiveCloud" in stage "PRODUCTION" with locked set to true
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "ActiveCloud"
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "target service is not deprecated"
|
||||
|
||||
Scenario: Declaration against a locked deprecated service is rejected
|
||||
Given a service "LockedCloud" in stage "DEPRECATED" with locked set to true
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "LockedCloud"
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "target service lock has not been released"
|
||||
|
||||
Scenario: Multiple replacement providers for the same deprecated service
|
||||
Given a service "CloudBridge" in stage "PRODUCTION" with O-level "LEGAL_ENTITY_VERIFIED"
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "SmartHub Cloud"
|
||||
And "CloudBridge" declares replacesServiceIds containing the ID of "SmartHub Cloud"
|
||||
Then GET /services/{smartHubCloudId}/replacements returns 2 candidates
|
||||
And "CloudBridge" appears before "OpenHub" in the results
|
||||
And the ordering is by O-level descending
|
||||
|
||||
Scenario: Replacement provider retracts their compatibility declaration
|
||||
Given "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
When "OpenHub" removes "SmartHub Cloud" from its replacesServiceIds
|
||||
Then GET /services/{smartHubCloudId}/replacements no longer includes "OpenHub"
|
||||
And the service_replacements row for this pair is deleted
|
||||
|
||||
Scenario: Duplicate declaration is idempotent
|
||||
Given "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "SmartHub Cloud" again
|
||||
Then the response is HTTP 200
|
||||
And GET /services/{smartHubCloudId}/replacements still returns exactly 1 candidate
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
Feature: Device owner discovers replacement services
|
||||
As an IoT device owner
|
||||
I want to discover compatible replacement services by polling APIX
|
||||
Without revealing my identity or device details
|
||||
|
||||
Background:
|
||||
Given a deprecated service "SmartHub Cloud" with locked set to false and a sunset_date set
|
||||
And "OpenHub" in stage "PRODUCTION" with O-level "IDENTITY_VERIFIED" has declared compatibility
|
||||
And "CloudBridge" in stage "PRODUCTION" with O-level "LEGAL_ENTITY_VERIFIED" has declared compatibility
|
||||
|
||||
Scenario: Device polls service status without authentication
|
||||
When GET /services/{smartHubCloudId} is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response body contains service_stage "DEPRECATED"
|
||||
And the response body contains locked false
|
||||
And the response body contains a sunset_date
|
||||
|
||||
Scenario: Device discovers replacement candidates without authentication
|
||||
When GET /services/{smartHubCloudId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response contains 2 candidates
|
||||
And "CloudBridge" appears before "OpenHub" in the results
|
||||
|
||||
Scenario: Device filters candidates by minimum O-level
|
||||
When GET /services/{smartHubCloudId}/replacements?minOLevel=LEGAL_ENTITY_VERIFIED is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response contains 1 candidate
|
||||
And the candidate is "CloudBridge"
|
||||
|
||||
Scenario: Device polls a still-locked deprecated service
|
||||
Given a deprecated service "LockedCloud" with locked set to true
|
||||
When GET /services/{lockedCloudId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response body contains an empty candidates list
|
||||
And the response body contains locked true
|
||||
|
||||
Scenario: Default production search excludes deprecated services
|
||||
When GET /services?capability=device.telemetry is called with no authentication header
|
||||
Then "SmartHub Cloud" is not in the results
|
||||
And only services with stage "PRODUCTION" are returned
|
||||
|
||||
Scenario: Explicit deprecated filter includes deprecated services
|
||||
When GET /services?capability=device.telemetry&stage=deprecated is called
|
||||
Then "SmartHub Cloud" is in the results
|
||||
And each result has service_stage "DEPRECATED"
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
Feature: Sunset declaration and lock release
|
||||
As a template owner
|
||||
I want to declare a sunset date and release the device lock
|
||||
So that IoT device owners can prepare for migration in a predictable window
|
||||
|
||||
Background:
|
||||
Given a registered service "SmartHub Cloud" with endpoint "https://api.smarthub.example"
|
||||
And the service has capability "device.telemetry"
|
||||
And the service is in stage "PRODUCTION"
|
||||
And the service has locked set to true
|
||||
|
||||
Scenario: Template owner declares sunset date
|
||||
When the template owner updates the service with sunset_date 90 days from now and stage "DEPRECATED"
|
||||
Then the service stage is "DEPRECATED"
|
||||
And the service has a sunset_date set
|
||||
And a version history entry of type "SUNSET_DECLARED" exists for the service
|
||||
And the service does not appear in default production search results for capability "device.telemetry"
|
||||
And the service appears in search results when stage filter is "deprecated"
|
||||
|
||||
Scenario: Sunset declaration preserves the device lock
|
||||
When the template owner updates the service with sunset_date 90 days from now and stage "DEPRECATED"
|
||||
Then the service has locked set to true
|
||||
And GET /services/{id}/replacements returns HTTP 200 with an empty list
|
||||
|
||||
Scenario: Template owner releases the device lock after sunset is declared
|
||||
Given the service is in stage "DEPRECATED" with a sunset_date set
|
||||
When the template owner sets locked to false
|
||||
Then the service has locked set to false
|
||||
And a version history entry of type "LOCK_RELEASED" exists for the service
|
||||
And the version history entry contains the previous locked value true
|
||||
|
||||
Scenario: Lock cannot be released without a prior sunset declaration
|
||||
When the template owner attempts to set locked to false without a sunset_date
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "sunset_at required before lock release"
|
||||
|
||||
Scenario: Sunset date cannot be set in the past
|
||||
When the template owner attempts to set sunset_date to yesterday
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "sunset_at must be a future moment"
|
||||
Reference in New Issue
Block a user