Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude/skills/dedalus-python-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import os
from dedalus_sdk import Dedalus

client = Dedalus(
x_api_key=os.environ.get("DEDALUS_X_API_KEY"),
browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"),
)
```

Provide credentials using the options below. Environment variables are read automatically when the target runtime supports them:

- `x_api_key` (env: `DEDALUS_X_API_KEY`) — Dedalus API key. Alternative to Bearer token.
- `api_key` (env: `DEDALUS_API_KEY`) — Dedalus API key or short-lived delegated access token in Authorization: Bearer <credential>.
- `browser_token` (env: `DEDALUS_BROWSER_TOKEN`) — Credential for the BrowserAuth scheme.

## Calling operations

Expand All @@ -38,7 +39,7 @@ import os
from dedalus_sdk import Dedalus

client = Dedalus(
x_api_key=os.environ.get("DEDALUS_X_API_KEY"),
browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"),
)

machine = client.machines.create(
Expand Down Expand Up @@ -84,7 +85,7 @@ except APIStatusError as err:

## Requirements

- Python 3.8 or newer
- Python 3.9 or newer

## Reference files

Expand Down
40 changes: 33 additions & 7 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,44 @@ jobs:
# version bump and changelog must also land back on scalar-next, or the next
# release PR would propose this release again. A real merge (never a plain
# commit push) so a squash- or rebase-merged release PR syncs just as well; the
# non-conventional message keeps the sync commit out of future changelogs. Plain
# (non-force) push on purpose: losing a race against a concurrent regeneration
# push fails loudly here, and the platform's next sync re-converges.
# non-conventional message keeps the sync commit out of future changelogs.
#
# The Scalar platform merges main into scalar-next itself when the release
# PR merges, so this step routinely races it. The sync is done once the release commit
# is an ancestor of scalar-next, whoever merged it, so that is checked before every
# attempt. A rejected push retries from a fresh fetch rather than failing the release
# run. Plain (non-force) push on purpose: a concurrent regeneration push is never
# discarded, and a sync that still cannot land after the retries fails loudly.
- name: Sync release back to scalar-next
if: ${{ steps.release.outputs.release_created == 'true' }}
env:
RELEASE_SHA: ${{ steps.release.outputs.sha }}
RELEASE_TAG: ${{ steps.release.outputs.tag_name }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin scalar-next
git checkout -B scalar-next origin/scalar-next
git merge --no-edit -m "Sync release ${{ steps.release.outputs.tag_name }} back to scalar-next" "${{ steps.release.outputs.sha }}"
git push origin scalar-next
# Explicit `|| return 1`s because `set -e` does not apply inside a function called
# as a condition, and a failed merge must never fall through to a no-op push.
sync_release() {
git fetch origin scalar-next || return 1
if git merge-base --is-ancestor "$RELEASE_SHA" origin/scalar-next; then
echo "$RELEASE_TAG is already on scalar-next."
return 0
fi
git checkout -B scalar-next origin/scalar-next || return 1
if ! git merge --no-edit -m "Sync release $RELEASE_TAG back to scalar-next" "$RELEASE_SHA"; then
git merge --abort
return 1
fi
git push origin scalar-next
}
for attempt in 1 2 3; do
if sync_release; then exit 0; fi
echo "Attempt $attempt of 3 to sync $RELEASE_TAG back to scalar-next failed."
if [ "$attempt" -lt 3 ]; then sleep $((attempt * 5)); fi
done
echo "::error::Could not sync $RELEASE_TAG back to scalar-next after 3 attempts."
exit 1
publish:
needs: release-please
# `!cancelled()` keeps publishing even when the back-sync to the integration branch
Expand Down
138 changes: 44 additions & 94 deletions .github/workflows/release-title-edit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ name: Release PR Version

on:
# `edited` is not one of the default pull_request types, but it is the point of this
# workflow: retitling the release PR is how a maintainer picks an exact version. The
# other types keep the consistency check attached to the PR as it evolves.
# workflow: retitling the release PR is how a maintainer picks an exact version, and the
# check has to re-run on the new title. The other types keep the consistency check attached
# to the PR as it evolves.
pull_request:
types: [opened, reopened, edited, synchronize]

# Read-only by default; only the job that pushes the Release-As commit widens this.
# Read-only: the check reads the committed version, and polls the pull request while it waits
# for a re-render.
permissions:
contents: read
pull-requests: read

jobs:
version-consistency:
Expand All @@ -25,30 +28,39 @@ jobs:
# typo'd into the title still reaches the script and fails there, rather than falling out
# of the guard and leaving no check at all — a missing check is not a failing one, and this
# is the check the release PR's footer tells maintainers to wait for before merging.
#
# It is also what keeps the workflow from failing silently if the platform ever opens
# release PRs from a different account: the bridge below stops firing, but this check still
# runs, the retitled version and the committed one disagree, and it turns red. The merge is
# blocked loudly instead of releasing the old version.
if: >-
${{ github.event.pull_request.base.ref == 'main'
&& startsWith(github.event.pull_request.title, 'release: ') }}
runs-on: ubuntu-latest
# Bounds the wait below with room to spare; a re-render normally lands within a minute.
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The version committed on the PR head is what merging would actually release.
ref: ${{ github.event.pull_request.head.sha }}
# Fails while the title version and the committed version disagree — the window between
# a maintainer's retitle and the platform re-rendering the PR from it. Without this a
# merge in that window would tag a release whose own files self-report the old version,
# and a title release-please cannot parse would silently cut no release at all.
# A retitle is applied by the Scalar platform, which pushes a Release-As commit to the
# integration branch and re-renders the release PR at that version. Until it lands, the
# title and the committed version disagree, and a merge in that window would tag a release
# whose own files self-report the old version.
#
# So this waits rather than failing: the check stays pending, which blocks a merge just as
# a failure would where the check is required, but does not flash red on every retitle. It
# passes once the title and the committed version agree, and hands over once the head
# moves, since the re-render's push starts a run of its own on the new head. It never
# passes while the title still disagrees, even a newer title: the run started for that
# edit judges it, and a pass here would sit beside it on the same head. Only a re-render
# that never arrives, or a title that is not a version at all, fails it.
- name: Compare the title version with the committed version
env:
# The title is human input, so it is bound through the environment (never
# interpolated into the script) and matched against a strict semver pattern
# before it is read.
PR_TITLE: ${{ github.event.pull_request.title }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
pattern='^release: ((0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$'
if [[ ! "$PR_TITLE" =~ $pattern ]]; then
Expand All @@ -57,87 +69,25 @@ jobs:
fi
title_version="${BASH_REMATCH[1]}"
manifest_version="$(jq -r '.["."]' .release-please-manifest.json)"
if [[ "$title_version" != "$manifest_version" ]]; then
echo "::error::Release PR title says $title_version but the pull request is versioned $manifest_version. Wait for the release PR to be re-rendered at $title_version before merging."
exit 1
fi
echo "Release PR title and committed version agree on $title_version."

apply-title-version:
name: Apply edited release version
# A human retitle of the open release PR is bridged into the canonical explicit-version
# mechanism (a Release-As commit on scalar-next, which the platform re-renders the
# release PR from). `changes.title` is only set when the title itself changed, and bot
# senders are ignored so the platform's own retitles cannot bounce back into another
# commit.
#
# Both signals are required here, unlike the read-only check above, and each covers what the
# other cannot. The author, because this job pushes with `contents: write` and a title is
# free text — anyone able to open a pull request against main could otherwise
# name a version and have it committed to scalar-next. `user.login` is set by GitHub when
# the pull request is opened and cannot be forged by whoever edits the title afterwards.
# The title, because those accounts open pull requests other than release PRs, and retitling
# one of those must not push a release.
#
# One login per platform deployment, since a repo generated by staging carries staging's
# app. Parenthesised because the group is ANDed with the title clause below: without the
# parens that `&&` would bind to the last login alone, letting the other accounts push a
# release off any title. Listed rather than matched on the shared `scalar-docs` stem — a
# prefix test would also admit any future `scalar-docs-*[bot]`, including someone else's.
if: >-
${{ github.event.action == 'edited'
&& github.event.changes.title != null
&& github.event.sender.type != 'Bot'
&& github.event.pull_request.state == 'open'
&& github.event.pull_request.base.ref == 'main'
&& (github.event.pull_request.user.login == 'scalar-docs[bot]'
|| github.event.pull_request.user.login == 'scalar-docs-staging[bot]'
|| github.event.pull_request.user.login == 'scalar-docs-development[bot]')
&& startsWith(github.event.pull_request.title, 'release: ') }}
# One bridge run at a time per pull request, newest retitle wins. Two retitles in quick
# succession (a version typo corrected seconds later) would otherwise start two runs that
# both read the version committed on the PR head, both get past the no-op guard, and both
# push — and since each fetches scalar-next immediately before committing, the second
# push fast-forwards instead of failing. release-please honours the newest Release-As
# footer, so the release would land on whichever run happened to push last, not on the
# title the maintainer actually left behind.
concurrency:
group: release-title-edit-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The PR head, so the version the pull request currently carries can be read before
# deciding whether anything needs to change.
ref: ${{ github.event.pull_request.head.sha }}
- name: Push a Release-As commit for the edited version
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
pattern='^release: ((0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$'
if [[ ! "$PR_TITLE" =~ $pattern ]]; then
echo "::error::Release PR title must be \"release: X.Y.Z\" with a full semver version, got: $PR_TITLE"
exit 1
fi
# Only ever a validated semver from here on, so it is safe in a commit message.
version="${BASH_REMATCH[1]}"
manifest_version="$(jq -r '.["."]' .release-please-manifest.json)"
# The re-rendered PR is retitled to the version it now carries; stopping here keeps
# that from producing an endless chain of Release-As commits.
if [[ "$version" == "$manifest_version" ]]; then
echo "This release PR already carries $version; nothing to do."
if [[ "$title_version" == "$manifest_version" ]]; then
echo "Release PR title and committed version agree on $title_version."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin scalar-next
git checkout -B scalar-next origin/scalar-next
git commit --allow-empty -m "chore: release $version" -m "Release-As: $version"
# Plain (non-force) push: losing a race against a regeneration push fails loudly
# rather than discarding it, and the retitle can simply be repeated.
git push origin scalar-next
echo "Pushed Release-As: $version to scalar-next. The release PR will be re-rendered at that version."
echo "Release PR title says $title_version but the pull request is versioned $manifest_version. Waiting for it to be re-rendered at $title_version."
# Up to ten minutes: 60 polls, 10 seconds apart.
for _ in $(seq 60); do
sleep 10
# A failed read is retried on the next poll rather than failing the check.
current="$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq '[.head.sha, .title] | @tsv')" || continue
IFS=$'\t' read -r current_sha current_title <<< "$current"
if [[ "$current_sha" != "$HEAD_SHA" ]]; then
echo "The release PR has a new head; the run for that push checks it."
exit 0
fi
if [[ "$current_title" =~ $pattern && "${BASH_REMATCH[1]}" == "$manifest_version" ]]; then
echo "Release PR title and committed version agree on $manifest_version."
exit 0
fi
done
echo "::error::Release PR title says $title_version but the pull request is still versioned $manifest_version after 10 minutes. Rebuild the SDK from the Scalar dashboard to re-render it."
exit 1
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.6.0"
".": "0.7.0"
}
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [0.7.0](https://github.com/dedalus-labs/dedalus-python/compare/v0.6.0...v0.7.0) (2026-10-28)


### Features

* **api:** add auth scheme BrowserAuth (+1 more change) ([b016780](https://github.com/dedalus-labs/dedalus-python/commit/b016780a061a08941ca7d8e935e266cfecc4dabc))


### Chores

* **api:** update generated SDK content ([61afcb0](https://github.com/dedalus-labs/dedalus-python/commit/61afcb0d0783d257542428550145370ffd04fce9))

## [0.6.0](https://github.com/dedalus-labs/dedalus-python/compare/v0.5.0...v0.6.0) (2026-09-24)


Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import os
from dedalus_sdk import Dedalus

client = Dedalus(
x_api_key=os.environ.get("DEDALUS_X_API_KEY"),
browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"),
)

machine = client.machines.create(
Expand Down Expand Up @@ -91,13 +91,15 @@ Pass credentials to the generated client constructor. Environment variables are

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `x_api_key` | `string \| provider` | - | Dedalus API key. Alternative to Bearer token. Defaults to DEDALUS_X_API_KEY. |
| `api_key` | `string \| provider` | - | Dedalus API key or short-lived delegated access token in Authorization: Bearer <credential>. Defaults to DEDALUS_API_KEY. |
| `x_api_key` | `str \| None` | - | Dedalus API key. Alternative to Bearer token. Defaults to DEDALUS_X_API_KEY. |
| `api_key` | `str \| None` | - | Dedalus API key or short-lived delegated access token in Authorization: Bearer \<credential>. Defaults to DEDALUS_API_KEY. |
| `browser_token` | `str \| None` | - | Credential for the BrowserAuth scheme. Defaults to DEDALUS_BROWSER_TOKEN. |

Declared schemes:

- `ApiKeyAuth` API key in header `x-api-key`
- `BearerAuth` bearer token
- `BrowserAuth` OAuth2/OpenID Connect

<br />

Expand Down Expand Up @@ -141,7 +143,8 @@ client = Dedalus(
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `x_api_key` | `str \| None` | `os.environ.get("DEDALUS_X_API_KEY")` | Dedalus API key. Alternative to Bearer token. |
| `api_key` | `str \| None` | `os.environ.get("DEDALUS_API_KEY")` | Dedalus API key or short-lived delegated access token in Authorization: Bearer <credential>. |
| `api_key` | `str \| None` | `os.environ.get("DEDALUS_API_KEY")` | Dedalus API key or short-lived delegated access token in Authorization: Bearer \<credential>. |
| `browser_token` | `str \| None` | `os.environ.get("DEDALUS_BROWSER_TOKEN")` | Credential for the BrowserAuth scheme. |
| `base_url` | `str \| httpx.URL \| None` | - | Override the default API base URL. |
| `timeout` | `float \| Timeout \| None` | `60.0` | Maximum time in seconds to wait for a response before aborting a request. |
| `max_retries` | `int` | `2` | Number of retries for temporary failures. |
Expand Down Expand Up @@ -182,6 +185,6 @@ page = client.machines.list()

## Requirements

- Python 3.8 or newer
- Python 3.9 or newer

Powered by Scalar.
7 changes: 4 additions & 3 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import os
from dedalus_sdk import Dedalus

client = Dedalus(
x_api_key=os.environ.get("DEDALUS_X_API_KEY"),
browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"),
)
```

Provide credentials using the options below. Environment variables are read automatically when the target runtime supports them:

- `x_api_key` (env: `DEDALUS_X_API_KEY`) — Dedalus API key. Alternative to Bearer token.
- `api_key` (env: `DEDALUS_API_KEY`) — Dedalus API key or short-lived delegated access token in Authorization: Bearer <credential>.
- `browser_token` (env: `DEDALUS_BROWSER_TOKEN`) — Credential for the BrowserAuth scheme.

## Calling operations

Expand All @@ -38,7 +39,7 @@ import os
from dedalus_sdk import Dedalus

client = Dedalus(
x_api_key=os.environ.get("DEDALUS_X_API_KEY"),
browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"),
)

machine = client.machines.create(
Expand Down Expand Up @@ -84,7 +85,7 @@ except APIStatusError as err:

## Requirements

- Python 3.8 or newer
- Python 3.9 or newer

## Reference files

Expand Down
Loading
Loading