.github/workflows/api_cd.yaml #138
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| on: | |
| workflow_dispatch: | |
| env: | |
| INFISICAL_ENV: prod | |
| jobs: | |
| compute-version: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - run: git fetch --tags --force | |
| - uses: ./.github/actions/doxxer_install | |
| - id: version | |
| run: | | |
| VERSION=$(doxxer --config doxxer.api.toml next patch) | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Computed version: $VERSION" | |
| deploy: | |
| needs: compute-version | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| concurrency: api-fly-deploy | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: superfly/flyctl-actions/setup-flyctl@master | |
| - uses: ./.github/actions/rust_install | |
| with: | |
| platform: linux | |
| - uses: ./.github/actions/infisical_install | |
| # Machine-identity OIDC login instead of a static service token: scopes | |
| # live in the identity's editable project role, so secret reorganizations | |
| # never require re-minting a token into GitHub secrets. | |
| - name: Infisical OIDC login | |
| run: | | |
| set -euo pipefail | |
| oidc_jwt=$(curl -sSf -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ | |
| "${ACTIONS_ID_TOKEN_REQUEST_URL}" | jq -r '.value') | |
| infisical_token=$(infisical login --method=oidc-auth \ | |
| --machine-identity-id=7dde9440-cec4-4cf0-a13f-1588ba67feb2 \ | |
| --jwt="$oidc_jwt" --silent --plain) | |
| echo "::add-mask::$infisical_token" | |
| echo "INFISICAL_TOKEN=$infisical_token" >> "$GITHUB_ENV" | |
| - name: Verify encrypted CloudSync | |
| run: | | |
| set -euo pipefail | |
| python3 .github/scripts/test_verify_cloudsync_e2ee.py | |
| cloudsync_secrets_json="$(mktemp)" | |
| trap 'rm -f "$cloudsync_secrets_json"' EXIT | |
| infisical export \ | |
| --token="$INFISICAL_TOKEN" \ | |
| --env="$INFISICAL_ENV" \ | |
| --projectId="$INFISICAL_PROJECT_ID" \ | |
| --path="/anarlog/cloudsync" \ | |
| --format=json \ | |
| --output-file="$cloudsync_secrets_json" | |
| python3 .github/scripts/verify_cloudsync_e2ee.py "$cloudsync_secrets_json" | |
| env: | |
| INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }} | |
| - run: | | |
| set -euo pipefail | |
| ai_secrets_json="$(mktemp)" | |
| cloudsync_secrets_json="$(mktemp)" | |
| secrets_file="$(mktemp)" | |
| trap 'rm -f "$ai_secrets_json" "$cloudsync_secrets_json" "$secrets_file"' EXIT | |
| infisical export \ | |
| --token="$INFISICAL_TOKEN" \ | |
| --env="$INFISICAL_ENV" \ | |
| --projectId="$INFISICAL_PROJECT_ID" \ | |
| --path="/anarlog/ai" \ | |
| --format=json \ | |
| --output-file="$ai_secrets_json" | |
| infisical export \ | |
| --token="$INFISICAL_TOKEN" \ | |
| --env="$INFISICAL_ENV" \ | |
| --projectId="$INFISICAL_PROJECT_ID" \ | |
| --path="/anarlog/cloudsync" \ | |
| --format=json \ | |
| --output-file="$cloudsync_secrets_json" | |
| python3 - "$ai_secrets_json" "$cloudsync_secrets_json" "$secrets_file" <<'PY' | |
| import json | |
| import sys | |
| cloudsync_runtime_keys = { | |
| "ANARLOG_CLOUDSYNC_DATABASE_ID", | |
| "ANARLOG_CLOUDSYNC_E2EE_DATABASE_ID", | |
| "ANARLOG_CLOUDSYNC_PROTOCOL_MODE", | |
| "ANARLOG_CLOUDSYNC_TOKEN_TTL_SECONDS", | |
| "SQLITECLOUD_PROJECT_URL", | |
| "SQLITECLOUD_TOKEN_ISSUER_API_KEY", | |
| } | |
| sources = ( | |
| (sys.argv[1], None), | |
| (sys.argv[2], cloudsync_runtime_keys), | |
| ) | |
| secrets_by_key = {} | |
| cloudsync_keys = set() | |
| for source_path, allowed_keys in sources: | |
| with open(source_path) as source: | |
| for secret in json.load(source): | |
| key = secret["key"] | |
| if allowed_keys is not None and key not in allowed_keys: | |
| continue | |
| value = secret.get("value", "") | |
| if allowed_keys is cloudsync_runtime_keys and value: | |
| cloudsync_keys.add(key) | |
| if key in secrets_by_key and secrets_by_key[key] != value: | |
| raise ValueError(f"Conflicting Infisical secret: {key}") | |
| secrets_by_key[key] = value | |
| missing_cloudsync_keys = sorted(cloudsync_runtime_keys - cloudsync_keys) | |
| if missing_cloudsync_keys: | |
| raise ValueError( | |
| "Missing required CloudSync secrets: " + ", ".join(missing_cloudsync_keys) | |
| ) | |
| with open(sys.argv[-1], "w") as target: | |
| for key, value in secrets_by_key.items(): | |
| value = value.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "\\n") | |
| target.write(f"{key}={value}\n") | |
| PY | |
| if [ -s "$secrets_file" ]; then | |
| flyctl secrets import --app hyprnote-ai --stage < "$secrets_file" | |
| else | |
| echo "No Infisical secrets exported; skipping Fly secret import." | |
| fi | |
| env: | |
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | |
| INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }} | |
| - run: flyctl deploy --config apps/api/fly.toml --dockerfile apps/api/Dockerfile --remote-only --build-arg APP_VERSION=${{ needs.compute-version.outputs.version }} | |
| env: | |
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | |
| - if: failure() | |
| run: timeout 60s flyctl logs --app hyprnote-ai --no-tail || true | |
| env: | |
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | |
| tag: | |
| needs: [compute-version, deploy] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: mathieudutour/github-tag-action@v6.2 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| custom_tag: api_v${{ needs.compute-version.outputs.version }} | |
| tag_prefix: "" |