Skip to content
Merged
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
89 changes: 41 additions & 48 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -580,14 +580,26 @@ jobs:
release-plz-release:
name: release-plz release
needs: [candidate, qualify]
if: needs.candidate.outputs.candidate == 'true' && needs.qualify.result == 'success' && needs.qualify.outputs.revision == needs.candidate.outputs.revision
if: >-
github.event_name == 'push' &&
github.repository_id == '1300766683' &&
github.ref == 'refs/heads/main' &&
needs.candidate.outputs.candidate == 'true' &&
needs.qualify.result == 'success' &&
needs.qualify.outputs.revision == needs.candidate.outputs.revision
runs-on: ubuntu-latest
timeout-minutes: 30
environment: release
permissions:
contents: write # Create the git tag, GitHub release, and package release commit.
contents: read # The App token authorizes publication under docs/adr/sync-release-app-credentials-to-the-protected-environment.md.
pull-requests: read # Revalidate the associated release pull request.
id-token: write # Authenticate to the automation credential source.
id-token: write # Keep registry authentication under docs/adr/sync-release-app-credentials-to-the-protected-environment.md.
env:
APP_INPUTS_VALID: >-
${{ secrets.RELEASE_APP_CLIENT_ID != '' &&
!startsWith(secrets.RELEASE_APP_CLIENT_ID, '${') &&
secrets.RELEASE_APP_PRIVATE_KEY != '' &&
!startsWith(secrets.RELEASE_APP_PRIVATE_KEY, '${') }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -604,30 +616,17 @@ jobs:
GH_TOKEN: ${{ github.token }}
RELEASE_CONFIG: ${{ runner.temp }}/qualified-release-plz.toml
run: cargo run --locked --quiet -p xtask -- ci publish-source --repository "$GH_REPO" --revision "$CANDIDATE_SHA" --config "$RELEASE_CONFIG" --output "$GITHUB_OUTPUT"
- name: Fetch automation credentials
id: doppler_automation_0
uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534
with:
auth-method: oidc
doppler-identity-id: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID }}
doppler-project: webpkit
doppler-config: automation
inject-env-vars: false
- name: Reject missing or unresolved automation credentials
if: >-
${{ steps.doppler_automation_0.outcome == 'success' && (
steps.doppler_automation_0.outputs.GITHUB_APP_CLIENT_ID == '' ||
startsWith(steps.doppler_automation_0.outputs.GITHUB_APP_CLIENT_ID, '${') ||
steps.doppler_automation_0.outputs.GITHUB_APP_PRIVATE_KEY == '' ||
startsWith(steps.doppler_automation_0.outputs.GITHUB_APP_PRIVATE_KEY, '${')
) }}
if: env.APP_INPUTS_VALID != 'true'
shell: bash
run: exit 1
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
id: app-token
with:
client-id: ${{ steps.doppler_automation_0.outputs.GITHUB_APP_CLIENT_ID }}
private-key: ${{ steps.doppler_automation_0.outputs.GITHUB_APP_PRIVATE_KEY }}
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: webpkit
permission-contents: write
permission-pull-requests: read
- name: verify the publisher source immediately before release
Expand All @@ -644,50 +643,44 @@ jobs:

release-plz-pr:
name: release-plz pr
if: >-
github.event_name == 'push' &&
github.repository_id == '1300766683' &&
github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
environment: release
permissions:
contents: write # Open and update the release pull request.
pull-requests: write # Create and update the release pull request.
id-token: write # Authenticate to the automation credential source.
contents: read # The App token authorizes proposals under docs/adr/sync-release-app-credentials-to-the-protected-environment.md.
env:
APP_INPUTS_VALID: >-
${{ secrets.RELEASE_APP_CLIENT_ID != '' &&
!startsWith(secrets.RELEASE_APP_CLIENT_ID, '${') &&
secrets.RELEASE_APP_PRIVATE_KEY != '' &&
!startsWith(secrets.RELEASE_APP_PRIVATE_KEY, '${') }}
concurrency:
group: release-plz-pr
cancel-in-progress: false
steps:
- name: Fetch automation credentials
id: doppler_automation_0
uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534
with:
auth-method: oidc
doppler-identity-id: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID }}
doppler-project: webpkit
doppler-config: automation
inject-env-vars: false
- name: Reject missing or unresolved automation credentials
if: >-
${{ steps.doppler_automation_0.outcome == 'success' && (
steps.doppler_automation_0.outputs.GITHUB_APP_CLIENT_ID == '' ||
startsWith(steps.doppler_automation_0.outputs.GITHUB_APP_CLIENT_ID, '${') ||
steps.doppler_automation_0.outputs.GITHUB_APP_PRIVATE_KEY == '' ||
startsWith(steps.doppler_automation_0.outputs.GITHUB_APP_PRIVATE_KEY, '${')
) }}
if: env.APP_INPUTS_VALID != 'true'
shell: bash
run: exit 1
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
id: app-token
with:
client-id: ${{ steps.doppler_automation_0.outputs.GITHUB_APP_CLIENT_ID }}
private-key: ${{ steps.doppler_automation_0.outputs.GITHUB_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
- name: install the repository Rust toolchain
run: rustup toolchain install "$RUST_VERSION" --profile minimal --no-self-update
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
id: app-token
with:
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: webpkit
permission-contents: write
permission-pull-requests: write
- uses: release-plz/action@8b5bf233ecb5779f63508db7cd050e948ee77cc2 # v0.5.144
with:
command: release-pr
Expand Down
49 changes: 12 additions & 37 deletions .github/workflows/verify-doppler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,54 +18,29 @@ jobs:
name: Verify automation access
if: >-
github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main'
github.ref == 'refs/heads/main' &&
github.repository_id == '1300766683'
runs-on: ubuntu-26.04
timeout-minutes: 5
environment: release
permissions:
id-token: write
permissions: {}
env:
APP_INPUTS_VALID: >-
${{ secrets.RELEASE_APP_CLIENT_ID != '' &&
!startsWith(secrets.RELEASE_APP_CLIENT_ID, '${') &&
secrets.RELEASE_APP_PRIVATE_KEY != '' &&
!startsWith(secrets.RELEASE_APP_PRIVATE_KEY, '${') }}
steps:
- name: Fetch automation credentials
id: credentials
uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534
with:
auth-method: oidc
doppler-identity-id: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID }}
doppler-project: webpkit
doppler-config: automation
inject-env-vars: false
- name: Reject missing or unresolved automation credentials
if: >-
${{ steps.credentials.outcome == 'success' && (
steps.credentials.outputs.GITHUB_APP_CLIENT_ID == '' ||
startsWith(steps.credentials.outputs.GITHUB_APP_CLIENT_ID, '${') ||
steps.credentials.outputs.GITHUB_APP_PRIVATE_KEY == '' ||
startsWith(steps.credentials.outputs.GITHUB_APP_PRIVATE_KEY, '${')
) }}
if: env.APP_INPUTS_VALID != 'true'
shell: bash
run: exit 1

- name: Verify a read-only repository token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ steps.credentials.outputs.GITHUB_APP_CLIENT_ID }}
private-key: ${{ steps.credentials.outputs.GITHUB_APP_PRIVATE_KEY }}
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-contents: read

- name: Attempt unrelated credential access
id: forbidden
continue-on-error: true
uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534
with:
auth-method: oidc
doppler-identity-id: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID }}
doppler-project: aozora
doppler-config: automation
inject-env-vars: false

- name: Reject unrelated credential access
if: ${{ always() && steps.forbidden.outcome != 'failure' }}
shell: bash
run: exit 1
13 changes: 13 additions & 0 deletions automation/release-app-sync.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
workplace = "8f95af89faf8535b63a6"
repository = "P4suta/webpkit"
repository_id = 1300766683
owner_id = 42543015
environment = "release"
branches = ["main"]
project = "webpkit"
source_config = "automation"
config = "github_automation"

[fields]
RELEASE_APP_CLIENT_ID = "GITHUB_APP_CLIENT_ID"
RELEASE_APP_PRIVATE_KEY = "GITHUB_APP_PRIVATE_KEY"
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
date: 2026-11-09
status: proposed
---

# Sync release App credentials to the protected environment

## Context

The release App requires stored credentials.
Doppler's [identity limit](https://docs.doppler.com/docs/platform-limits) constrains fleet growth.
The release environment admits main and prevents administrator bypass.

## Decision

Use [native GitHub synchronization](https://docs.doppler.com/docs/github-actions) from a purpose-specific Doppler config into the existing environment.
Pass named secrets only to repository-scoped App token actions after tool setup.
Preserve exact-source release qualification and registry authentication.

## Alternatives

Doppler OIDC avoids GitHub copies and preserves workflow and event claims, but consumes identity capacity.
A service token adds another stored credential.

## Consequences

GitHub stores credential copies.
Protected main and environment policies replace Doppler claims.
The shared App key retains authority across its installations.
Reconsider when GitHub supports workload identity for the App.

## Confirmation

Verify synchronization and manual authentication from reviewed main before identity retirement.
Pause publishing during merge, then restore its previous workflow state.
Migration creates no release or tag.
43 changes: 20 additions & 23 deletions docs/credential-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,26 @@ doppler_automation_app: release_plz

# Credential delivery

GitHub Actions exchanges its workload identity for a short-lived Doppler token at the credential-consuming step.
Each purpose has its own read-only service account, config, and environment variable.
Shared signing credentials stay in the canonical signing project and are referenced from the consumer config.
The shared credential still carries the signing provider's authority after retrieval.
The release App credentials use Doppler's [native GitHub synchronization](https://docs.doppler.com/docs/github-actions).
The [delivery decision](adr/sync-release-app-credentials-to-the-protected-environment.md) governs the cutover.
Canonical values remain in Doppler, and a purpose-specific config references the automation fields.
The [manifest](../automation/release-app-sync.toml) identifies the repository, environment, and field mapping.

| Purpose | Doppler config | GitHub environment variable | Environments |
| --- | --- | --- | --- |
| automation | `webpkit.automation` | `DOPPLER_AUTOMATION_IDENTITY_ID` | release |
The release environment admits main jobs and prevents administrator bypass.
RELEASE_APP_CLIENT_ID references GITHUB_APP_CLIENT_ID, and RELEASE_APP_PRIVATE_KEY references GITHUB_APP_PRIVATE_KEY.
The workflow passes each named secret only to the App token action after tool setup.
Each token targets the current repository and requests the consumer's permissions.
The App private key retains authority across every installation of the shared App.
GitHub stores copies, and protected main and environment policies replace Doppler workflow and event claims.

The manual **Verify credential access** workflow checks required fields and rejects access to an unrelated config.
Automation also creates a repository-scoped App token with contents read access.
This workflow performs no signing, notarization, registry publication, release creation, or deployment.
Existing environment protections remain in force; their approval does not grant authority for another operation.
The manual **Verify credential access** workflow checks required fields and creates a repository token with contents read access only.
The action revokes the token during cleanup.
Run the workflow from reviewed main after merging.
Keep the existing Doppler identity until replacement authentication succeeds.
Retire the identity only after confirming that every consumer has migrated.

After the reviewed workflows reach the default branch, run the credential-access check for that exact revision.
Keep the current credential delivery until every purpose and environment succeeds and the denial checks pass.
Confirm an explicit access-denied response for each negative check.
Keep the selected identity fixed for that scope check.
A workflow can exchange for every identity whose claims it satisfies; separate accounts alone don't isolate stages with matching claim policies.
Network, authentication, and runner failures leave that evidence inconclusive.
Then disconnect the corresponding Doppler-to-GitHub sync while preserving its target fields, check for remaining consumers, and remove the obsolete GitHub fields and legacy configs.
An expired or revoked registry key is a retirement candidate and must not be copied into a replacement config.
Use the registry's native trusted publisher where supported; an unconfigured publisher stays unavailable until its owner configures the exact binding.

Store only field names, identity metadata, source revisions, and verification outcomes in issues and logs.
Keep credential values and native authentication state out of the repository.
Rotate canonical values in Doppler and verify synchronization and authentication before retiring the previous key.
Keep values and authentication state out of the repository and logs.
Publication keeps its exact-source qualification, registry authentication, and release environment.
Pause the release-plz workflow during migration merge and authentication verification, then restore its previous enabled state.
Credential migration creates no release or tag.
Loading