Skip to content

Commit e831414

Browse files
authored
SG-43825 Improve pipeline stability (#449)
* create new ephemeral project on pipeline run * use ephemeral project as control for schema visibility tests
1 parent aca9d94 commit e831414

4 files changed

Lines changed: 44 additions & 20 deletions

File tree

azure-pipelines-templates/run-tests.yml

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ jobs:
102102
--durations=0 \
103103
--nunit-xml=test-results.xml \
104104
--verbose \
105+
--reruns 1 \
106+
--reruns-delay 2 \
105107
env:
106108
# Tell Pytest that we're running in a CI environment
107109
CI: 1
@@ -123,20 +125,39 @@ jobs:
123125
# This will give a user name like 'something macOS 2.7'
124126
SG_HUMAN_NAME: $(python_api_human_name) ${{ parameters.os_name }} ${{ parameters.python_version }}
125127
SG_HUMAN_PASSWORD: $(python_api_human_password)
126-
# So, first, we need to make sure that two builds running at the same time do not manipulate
127-
# the same entities, so we're sandboxing build nodes based on their name.
128-
SG_PROJECT_NAME: Python API CI - $(Agent.Name)
129-
# The entities created and then reused between tests assume that the same user is always
130-
# manipulating them. Because different builds will be assigned different agents and therefore
131-
# different projects, it means each project needs to have an entity specific to a given user.
132-
# Again, this would have been a lot simpler if we could simply have had a login based on the
133-
# agent name, but alas, the agent name has a space in it which needs to be replaced to something
134-
# else and string substitution can't be made on build variables, only template parameters.
135-
SG_ASSET_CODE: CI-$(python_api_human_login)-${{ parameters.os_name }}-${{ parameters.python_version }}
136-
SG_VERSION_CODE: CI-$(python_api_human_login)-${{ parameters.os_name }}-${{ parameters.python_version }}
137-
SG_SHOT_CODE: CI-$(python_api_human_login)-${{ parameters.os_name }}-${{ parameters.python_version }}
138-
SG_TASK_CONTENT: CI-$(python_api_human_login)-${{ parameters.os_name }}-${{ parameters.python_version }}
139-
SG_PLAYLIST_CODE: CI-$(python_api_human_login)-${{ parameters.os_name }}-${{ parameters.python_version }}
128+
# Each job gets its own ephemeral project, scoped to this build + OS + Python version.
129+
# This eliminates state bleed between concurrent runs and across successive builds on the
130+
# same agent. The project is retired in the "Cleanup test project" step below.
131+
SG_PROJECT_NAME: Python API CI - $(Build.BuildId) - ${{ parameters.os_name }} - ${{ parameters.python_version }}
132+
133+
- task: PythonScript@0
134+
displayName: Cleanup test project
135+
condition: always()
136+
inputs:
137+
scriptSource: inline
138+
workingDirectory: $(Build.SourcesDirectory)
139+
script: |
140+
import os, shotgun_api3, sys
141+
sg = shotgun_api3.Shotgun(
142+
os.environ['SG_SERVER_URL'],
143+
script_name=os.environ['SG_SCRIPT_NAME'],
144+
api_key=os.environ['SG_API_KEY'],
145+
)
146+
project = sg.find_one(
147+
entity_type='Project',
148+
filters=[['name', 'is', os.environ['SG_PROJECT_NAME']]],
149+
)
150+
if not project:
151+
print('Project not found, nothing to clean up.')
152+
sys.exit(0)
153+
sg.delete(entity_type='Project', entity_id=project['id'])
154+
print('Retired project:', os.environ['SG_PROJECT_NAME'])
155+
env:
156+
PYTHONPATH: $(Build.SourcesDirectory)
157+
SG_SERVER_URL: $(ci_site)
158+
SG_SCRIPT_NAME: $(ci_site_script_name)
159+
SG_API_KEY: $(ci_site_script_key)
160+
SG_PROJECT_NAME: Python API CI - $(Build.BuildId) - ${{ parameters.os_name }} - ${{ parameters.python_version }}
140161

141162
# Explicit call to PublishTestResults@2 and PublishCodeCoverageResults@2 here
142163
# instead of relying on pytest-azurepipelines because pytest-azurepipelines

tests/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ def _setup_db(cls, config, sg):
289289
cls.human_user = _find_or_create_entity(sg, "HumanUser", data)
290290

291291
data = {"code": cls.config.asset_code, "project": cls.project}
292-
keys = ["code"]
292+
keys = ["code", "project"]
293293
cls.asset = _find_or_create_entity(sg, "Asset", data, keys)
294294

295295
data = {

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
pytest
1212
pytest-cov
1313
pytest-nunit
14+
pytest-rerunfailures

tests/test_api.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3548,12 +3548,14 @@ def test_modify_visibility(self):
35483548
if field_name not in schema:
35493549
self.sg.schema_field_create("Asset", "text", "Project Visibility Test")
35503550

3551-
# Grab any two projects that we can use for toggling the visible property with.
3552-
projects = self.sg.find(
3553-
"Project", [], order=[{"field_name": "id", "direction": "asc"}]
3551+
# Hide/show on the ephemeral CI project so concurrent matrix jobs do not race.
3552+
# Use the oldest site project as the control (find_one defaults to sorting by "id" ascending)
3553+
project_1 = self.project
3554+
project_2 = self.sg.find_one("Project", [["id", "is_not", project_1["id"]]])
3555+
self.assertIsNotNone(
3556+
project_2,
3557+
"A second project is required to test per-project field visibility.",
35543558
)
3555-
project_1 = projects[0]
3556-
project_2 = projects[1]
35573559

35583560
# First, reset the field visibility in a known state, i.e. visible for both projects,
35593561
# in case the last test run failed midway through.

0 commit comments

Comments
 (0)