Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
59 changes: 59 additions & 0 deletions .github/workflows/elixir-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Elixir SDK

on:
pull_request:
paths:
- "secretspec-ex/**"
- ".github/workflows/elixir-package.yml"
push:
tags: ["v**"]
workflow_call:
inputs:
pre_release:
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
package:
name: Build and test Elixir package
if: >-
inputs.pre_release || github.event_name != 'pull_request' ||
!startsWith(github.event.pull_request.title, 'Release ')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: erlef/setup-beam@v1
with:
otp-version: "28"
elixir-version: "1.19"
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Install Hex and Rebar
run: |
mix local.hex --force
mix local.rebar --force
- name: Build SecretSpec CLI executable
run: cargo build --release --bin secretspec --features cli
- name: Fetch Elixir dependencies
working-directory: secretspec-ex
run: mix deps.get
- name: Check formatting
working-directory: secretspec-ex
run: mix format --check-formatted
- name: Run tests
working-directory: secretspec-ex
run: mix test
- name: Build docs
working-directory: secretspec-ex
run: mix docs
- name: Validate package
working-directory: secretspec-ex
run: mix hex.build
11 changes: 11 additions & 0 deletions .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ on:
- "secretspec-dotnet/src/SecretSpec/SecretSpec.csproj"
- "secretspec-jvm/gradle.properties"
- "Package.swift"
- "secretspec-ex/**"
- ".github/workflows/elixir-package.yml"

permissions:
contents: read
Expand Down Expand Up @@ -83,6 +85,15 @@ jobs:
pre_release: true
publish: false

elixir:
name: Elixir SDK
needs: authorize
permissions:
contents: read
uses: ./.github/workflows/elixir-package.yml
Comment thread
sylvesterroos marked this conversation as resolved.
Resolved
with:
pre_release: true

python:
name: Python wheels
needs: authorize
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sdks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: "SDKs"

# Runs every language SDK's full test suite (unit + cross-language conformance +
# the schema/quicktype codegen pipeline) against a freshly built cdylib, so the
# Python/Go/Ruby/Node/Haskell/C#/PHP/Swift bindings cannot silently rot.
# Python/Go/Ruby/Node/Haskell/C#/PHP/Swift/Elixir bindings cannot silently rot.

on:
workflow_call:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Replaced the Elixir SDK's embedded native resolver with a pure Elixir `secretspec.resolver/1` IPC client. The 0.21+ package launches `secretspec serve` directly and requires a compatible SecretSpec executable.

- IPC resolution exposes optional non-secret revision tokens for downstream task
cache invalidation (0.21+). AWS Secrets Manager reports value-bound revisions,
Expand Down Expand Up @@ -237,6 +238,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
under profile `prd` names the same secret as `doppler://myapp/prd`, and a
cache at that pairing would overwrite or delete the secret it caches (0.21+).

- **Elixir SDK (0.21+)** (`secretspec-ex`) is now a pure Elixir client for `secretspec.resolver/1`. It launches `secretspec serve` directly and requires a compatible SecretSpec executable instead of shipping native code or NIF archives.

### Fixed

- IPC rejects incompatible secret representations before contacting providers,
Expand Down
8 changes: 5 additions & 3 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Cross-language conformance suite

Every SecretSpec SDK (Python, Go, Ruby, Node.js, Haskell, C#, PHP, and Swift
(0.18+)) is a thin client over the same resolver contract. This suite proves
Every SecretSpec SDK (Python, Go, Ruby, Node.js, Haskell, C#, PHP, Swift
(0.18+), JVM (0.20+), and Elixir (0.21+)) is a thin client over the same
resolver contract. This suite proves
they agree: each SDK resolves the same fixtures and must produce the identical
**canonical** result.

Expand Down Expand Up @@ -65,4 +66,5 @@ relative to the repo root:
`swift test --filter SecretSpecTests.testCrossLanguageConformance` after
staging the local XCFramework as described in `secretspec-swift/README.md`
- JVM (0.20+):
`cd secretspec-jvm && ./gradlew test`
`cd secretspec-jvm && ./gradlew test`
- Elixir (0.21+): `cd secretspec-ex && mix test test/conformance_test.exs`
8 changes: 8 additions & 0 deletions conformance/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ run_jvm() { (
cd secretspec-jvm
./gradlew test
); }
run_elixir() { (
cd secretspec-ex
mix local.hex --force
mix local.rebar --force
mix deps.get
mix test
); }

run "Python" python run_python
run "Go" go run_go
Expand All @@ -117,6 +124,7 @@ else
fi
# The gradle wrapper will download gradle, use "bash" as a placeholder
run "JVM" bash run_jvm
run "Elixir" mix run_elixir

echo
echo "==> Conformance summary"
Expand Down
15 changes: 14 additions & 1 deletion devenv.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{ lib, pkgs, ... }: {
{ lib, pkgs, ... }:
let
# https://nixos.org/manual/nixpkgs/stable/#beam-structure
beamPackages = pkgs.beam29Packages.overrideScope (self: super: { elixir = self.elixir_1_20; });
erlang = beamPackages.erlang;
elixir = beamPackages.elixir;
in
{
languages.rust = {
enable = true;
# The Rust version is pinned in rust-toolchain.toml, which the native CI
Expand Down Expand Up @@ -69,6 +76,10 @@
enable = true;
jdk.package = pkgs.jdk21;
};
languages.elixir = {
enable = true;
package = elixir;
};

packages = [
# documentation link validation
Expand Down Expand Up @@ -106,6 +117,8 @@
pkgs.ninja
# Installs the libsecretspec archive with its header and pkg-config file
pkgs.cargo-c
# expert-lsp needs the erlang binary
erlang
];

env = {
Expand Down
5 changes: 5 additions & 0 deletions docs/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,11 @@ Values can be resolved from: keyring (default), KeePass KDBX (0.17+), dotenv fil
slug: "sdk/jvm",
badge: { text: "0.20+", variant: "note" },
},
{
label: "Elixir",
slug: "sdk/elixir",
badge: { text: "0.21+", variant: "note" },
Comment thread
sylvesterroos marked this conversation as resolved.
Resolved
},
],
},
{
Expand Down
8 changes: 7 additions & 1 deletion docs/src/content/docs/development/sdks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: How the language SDKs are built, packaged, and released, and which
import VersionCompatibility from '@cachix/site-kit/ui/VersionCompatibility.astro';

SecretSpec ships SDKs for Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell,
PHP, C#, Swift (0.18+), and JVM languages (0.20+). This page is for
PHP, C#, Swift (0.18+), JVM languages (0.20+), and Elixir (0.21+). This page is for
contributors: how the SDKs are put together, how each one is packaged and
released, which platforms each artifact covers, and what to update when adding
a platform or a new SDK. For the user-facing architecture and API, see the
Expand Down Expand Up @@ -73,6 +73,11 @@ platform and publishes on a version tag:
| Swift (0.18+) | SwiftPM source package + XCFramework release asset | `swift-package.yml` |
| PHP | Composer package (source) + prebuilt extension binaries and `libsecretspec` release assets | `php-ext.yml`, `ffi-build.yml` |
| Haskell | `secretspec` on Hackage (source) | `haskell-build.yml` |
| Elixir (0.21+) | `secretspec` on Hex.pm, using `secretspec serve` | `elixir-package.yml` |

<VersionCompatibility version="0.21" />

The Elixir SDK is available from SecretSpec 0.21 and uses the shared resolver IPC protocol.

## Platform support

Expand All @@ -91,6 +96,7 @@ the Ruby gem, and the PHP extension binaries is added in SecretSpec 0.17.
| PHP | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — |
| Haskell (source) | ✓ (CI-covered) | — | — | — | ✓ (CI-covered, 0.17+) | — |
| JVM (0.20+) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Elixir (0.21+) | ✓ | ✓ | — | ✓ | ✓ | — |

Notes:

Expand Down
66 changes: 66 additions & 0 deletions docs/src/content/docs/sdk/elixir.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Elixir SDK
description: Resolve SecretSpec secrets from Elixir
---

import VersionCompatibility from '@cachix/site-kit/ui/VersionCompatibility.astro';

<VersionCompatibility version="0.21" />

The Elixir SDK (`secretspec`, 0.21+) is a pure Elixir client for the `secretspec.resolver/1` IPC protocol. It requires a compatible `secretspec` executable (0.21+) and launches `secretspec serve` directly, without a shell, NIF, native archive, or Rust code in the Hex package.

## Install

```elixir
defp deps do
[{:secretspec, "~> 0.21"}]
end
```

Install SecretSpec separately and make `secretspec` available on `PATH`, or pass an explicit absolute `executable:` option.

## Sessions

A session fixes the manifest, profile, scope, provider override, and reason for its lifetime. Resolution addresses one exact declared name at a time.

```elixir
{:ok, session} =
SecretSpec.Session.start_link(
manifest: "/project/secretspec.toml",
profile: "production",
scope: "deploy",
reason: "start application"
)

result = SecretSpec.Session.get(session, "DATABASE_URL",
representation: :value,
purpose: %{
consumer: "my_app",
operation: "connect",
host: "db.example.com"
}
)

SecretSpec.Session.close(session)
```

`representation` is `:auto`, `:value`, or `:path`. Path results are resolver-owned leases. Release them explicitly with `SecretSpec.Session.release/2`, or close the session to release all outstanding leases.

`purpose` must include non-empty `consumer` and `operation` fields. Optional `host` and `path` fields provide additional attribution.

## Results

- `{:ok, %SecretSpec.Secret{}}`
- `{:missing, required}`
- `:undeclared`
- `{:error, %SecretSpec.Error{}}`

Use `SecretSpec.with_session/2` for scoped lifetimes:

```elixir
SecretSpec.with_session([manifest: "/project/secretspec.toml"], fn session ->
SecretSpec.Session.get(session, "DATABASE_URL", purpose: %{consumer: "app", operation: "connect"})
end)
```

Whole-profile resolution, reports, `SecretSpec.Builder`, and `abi_version/0` are not part of the resolver protocol.
9 changes: 6 additions & 3 deletions docs/src/content/docs/sdk/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: How the SecretSpec language SDKs work
import VersionCompatibility from '@cachix/site-kit/ui/VersionCompatibility.astro';

SecretSpec ships SDKs for Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell,
PHP, C# (0.16+), Swift (0.18+) and JVM languages (0.20+). They all resolve
PHP, C# (0.16+), Swift (0.18+), JVM languages (0.20+), and Elixir (0.21+). They all resolve
secrets from the same declarative `secretspec.toml`, and they all behave
identically, because they share one resolver.

Expand Down Expand Up @@ -43,6 +43,7 @@ that core rather than a reimplementation:
and falls back to loading the same C ABI at runtime through `ext-ffi`.
- **JVM (0.20+)** loads the same C ABI with JNA from a runtime-specific
native asset in the Jar package.
- **Elixir (0.21+)** uses a pure Elixir client for `secretspec.resolver/1` and launches `secretspec serve` directly.

Because resolution happens in one place, every provider, chain, profile, and
generator works the same in every language, and a new provider added to the core
Expand Down Expand Up @@ -80,8 +81,8 @@ print(resolved.secrets["DATABASE_URL"].get)
See each language's page for the idiomatic spelling: [Rust](/sdk/rust),
[Python](/sdk/python), [Go](/sdk/go), [Ruby](/sdk/ruby),
[Node.js](/sdk/nodejs), [Haskell](/sdk/haskell), [PHP](/sdk/php),
[C# (0.16+)](/sdk/csharp), [Swift (0.18+)](/sdk/swift)
and [JVM languages (0.20+)](/sdk/jvm).
[C# (0.16+)](/sdk/csharp), [Swift (0.18+)](/sdk/swift),
[JVM languages (0.20+)](/sdk/jvm), and [Elixir (0.21+)](/sdk/elixir).

Every builder also takes a [scope (0.17+)](/concepts/scopes/),
resolving only a named subset of the profile and returning the selected name on
Expand Down Expand Up @@ -132,6 +133,7 @@ no runtime library path to set:
- **JVM** ships the `cdylib` as runtime-specific native assets in one
Jar package and loads the matching asset through JNA; glibc/musl Linux,
Intel/Arm macOS, and x64/Arm64 Windows assets are included.
- **Elixir (0.21+)** is architecture-independent and requires a compatible `secretspec` executable.

Because the resolver is linked or embedded directly, the SDKs do not depend on a
separately installed `cdylib` or an `LD_LIBRARY_PATH`/`SECRETSPEC_FFI_LIB`
Expand All @@ -156,6 +158,7 @@ SecretSpec 0.17.
| PHP | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — |
| Haskell (source) | ✓ | — | — | — | ✓ (0.17+) | — |
| JVM (0.20+) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Elixir (0.21+) | ✓ | ✓ | — | ✓ | ✓ | — |

Most Linux packages build against a manylinux_2_28 baseline (glibc 2.28 or
newer); the C# and JVM packages additionally ship musl Linux assets. Hackage
Expand Down
20 changes: 19 additions & 1 deletion docs/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ const consumerMetadata: ConsumerMetadata[] = [
secrets: ['DATABASE_URL', 'API_KEY', 'SENTRY_DSN'] },
{ icon: 'openjdk', label: 'JVM', href: '/sdk/jvm/',
secrets: ['DATABASE_URL', 'API_KEY', 'SENTRY_DSN'] },
{ icon: 'elixir', label: 'Elixir (0.21+)', href: '/sdk/elixir/',
secrets: ['DATABASE_URL', 'SECRET_KEY_BASE', 'API_KEY'] },
{ icon: 'githubactions', label: 'GitHub Actions', href: '/ci/github-actions/',
secrets: ['NPM_TOKEN', 'AWS_SECRET_ACCESS_KEY', 'CODECOV_TOKEN'] },
{ label: 'Forgejo Actions', href: '/ci/github-actions/',
Expand Down Expand Up @@ -185,6 +187,7 @@ const consumerColors: Record<string, SyntaxColorName> = {
Haskell: 'magenta',
Swift: 'red',
JVM: 'blue',
'Elixir (0.21+)': 'magenta',
'GitHub Actions': 'comment',
'Forgejo Actions': 'orange',
'devenv & Nix': 'cyan',
Expand Down Expand Up @@ -318,6 +321,21 @@ const languageSdkMetadata = [
<span class="tok-pun">.</span><span class="tok-fn">load</span><span class="tok-pun">();</span>
<span class="tok-var">System</span><span class="tok-pun">.</span><span class="tok-var">out</span><span class="tok-pun">.</span><span class="tok-fn">println</span><span class="tok-pun">(</span><span class="tok-var">s</span><span class="tok-pun">.</span><span class="tok-fn">secret</span><span class="tok-pun">(</span><span class="tok-str">"DATABASE_URL"</span><span class="tok-pun">).</span><span class="tok-fn">get</span><span class="tok-pun">());</span>`,
},
{
id: 'elixir',
icon: 'elixir',
label: 'Elixir (0.21+)',
href: '/sdk/elixir/',
color: consumerTone({ label: 'Elixir (0.21+)', href: '/sdk/elixir/', secrets: [] }),
code: `<span class="tok-var">resolved</span> <span class="tok-pun">=</span>
<span class="tok-var">SecretSpec</span><span class="tok-pun">.</span><span class="tok-fn">builder</span><span class="tok-pun">()</span>
<span class="tok-pun">|&gt;</span> <span class="tok-var">SecretSpec</span><span class="tok-pun">.</span><span class="tok-var">Builder</span><span class="tok-pun">.</span><span class="tok-fn">with_provider</span><span class="tok-pun">(</span><span class="tok-str">"keyring://"</span><span class="tok-pun">)</span>
<span class="tok-pun">|&gt;</span> <span class="tok-var">SecretSpec</span><span class="tok-pun">.</span><span class="tok-var">Builder</span><span class="tok-pun">.</span><span class="tok-fn">with_reason</span><span class="tok-pun">(</span><span class="tok-str">"boot"</span><span class="tok-pun">)</span>
<span class="tok-pun">|&gt;</span> <span class="tok-var">SecretSpec</span><span class="tok-pun">.</span><span class="tok-var">Builder</span><span class="tok-pun">.</span><span class="tok-fn">load</span><span class="tok-pun">()</span>

<span class="tok-var">resolved</span><span class="tok-pun">.</span><span class="tok-var">secrets</span><span class="tok-pun">[</span><span class="tok-str">"DATABASE_URL"</span><span class="tok-pun">]</span>
<span class="tok-pun">|&gt;</span> <span class="tok-var">SecretSpec</span><span class="tok-pun">.</span><span class="tok-var">ResolvedSecret</span><span class="tok-pun">.</span><span class="tok-fn">get</span><span class="tok-pun">()</span>`,
},
];

function providerTone(provider: ProviderMetadata) {
Expand Down Expand Up @@ -1839,7 +1857,7 @@ Enter access_token for provider 'bws': <span class="tok-str">****</span>
<span class="landing-showcase-eyebrow">Language SDKs</span>
<h2 class="landing-section-title">Resolve secrets directly from your language.</h2>
<p class="landing-section-lead landing-section-lead-narrow">
One resolver across {sdkCount} SDKs—Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, C#, Swift and JVM. Same profiles. Same providers. <a href="/sdk/overview/" class="landing-link csk-text-link">SDKs →</a>
One resolver across {sdkCount} SDKs—Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, C#, Swift, JVM and Elixir (0.21+). Same profiles. Same providers. <a href="/sdk/overview/" class="landing-link csk-text-link">SDKs →</a>
</p>
</div>

Expand Down
Loading
Loading