diff --git a/.github/workflows/deploy-studio-release-server.yaml b/.github/workflows/deploy-studio-release-server.yaml new file mode 100644 index 00000000..4e6531e0 --- /dev/null +++ b/.github/workflows/deploy-studio-release-server.yaml @@ -0,0 +1,49 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Deploy Studio Release Server + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy: + if: >- + github.repository == 'volcengine/veadk-python' && + github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: studio-release + env: + VOLCENGINE_ACCESS_KEY: ${{ secrets.VOLCENGINE_ACCESS_KEY }} + VOLCENGINE_SECRET_KEY: ${{ secrets.VOLCENGINE_SECRET_KEY }} + STUDIO_RELEASE_SERVER_API_KEY: ${{ secrets.STUDIO_RELEASE_SERVER_API_KEY }} + + steps: + - name: Check out deployment source + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Deploy release server + run: frontend/service/studio_release_server/deploy.sh --skip-github-secrets diff --git a/frontend/service/studio_release_server/deploy.py b/frontend/service/studio_release_server/deploy.py index 464d652f..231f5ab6 100644 --- a/frontend/service/studio_release_server/deploy.py +++ b/frontend/service/studio_release_server/deploy.py @@ -527,6 +527,19 @@ def _set_github_secret(name: str, value: str) -> None: raise RuntimeError(f"Could not set GitHub secret {name}: {completed.stderr}") +def _deployment_api_key(*, skip_github_secrets: bool) -> str: + """Reuse the configured API key when repository secrets stay unchanged.""" + if not skip_github_secrets: + return secrets.token_urlsafe(48) + api_key = os.getenv("STUDIO_RELEASE_SERVER_API_KEY", "").strip() + if len(api_key) < 32: + raise ValueError( + "STUDIO_RELEASE_SERVER_API_KEY must be provided when GitHub " + "Secrets are not updated." + ) + return api_key + + def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source-root", type=Path, default=Path.cwd()) @@ -550,7 +563,7 @@ def main() -> None: if not args.skip_github_secrets: _validate_github_secret_access() role_trn = _ensure_runtime_role(access_key, secret_key) - api_key = secrets.token_urlsafe(48) + api_key = _deployment_api_key(skip_github_secrets=args.skip_github_secrets) endpoint, app_id, function_id = _deploy(source_root, api_key, role_trn) _wait_for_health(endpoint, api_key) if not args.skip_github_secrets: diff --git a/tests/test_studio_release_server.py b/tests/test_studio_release_server.py index 5d4e6686..b5e327c5 100644 --- a/tests/test_studio_release_server.py +++ b/tests/test_studio_release_server.py @@ -551,6 +551,35 @@ def _run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: ] +def test_skip_github_secrets_reuses_existing_api_key( + monkeypatch: pytest.MonkeyPatch, +) -> None: + api_key = "existing-release-key-with-at-least-thirty-two-characters" + monkeypatch.setenv("STUDIO_RELEASE_SERVER_API_KEY", api_key) + + assert release_deploy._deployment_api_key(skip_github_secrets=True) == api_key + + +def test_skip_github_secrets_requires_existing_api_key( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("STUDIO_RELEASE_SERVER_API_KEY", raising=False) + + with pytest.raises(ValueError, match="STUDIO_RELEASE_SERVER_API_KEY"): + release_deploy._deployment_api_key(skip_github_secrets=True) + + +def test_release_server_deploy_workflow_preserves_api_key() -> None: + workflow = ( + Path(__file__).parents[1] + / ".github/workflows/deploy-studio-release-server.yaml" + ).read_text(encoding="utf-8") + + assert "workflow_dispatch:" in workflow + assert "secrets.STUDIO_RELEASE_SERVER_API_KEY" in workflow + assert "deploy.sh --skip-github-secrets" in workflow + + def test_release_server_readiness_uses_rotated_api_key( monkeypatch: pytest.MonkeyPatch, ) -> None: