From d2e1aae38dad843080ec28c142af56223dce215f Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:22:06 +0900 Subject: [PATCH] ci(secrets): sync protected release credentials --- .github/workflows/release-plz.yml | 89 +++++++++---------- .github/workflows/verify-doppler.yml | 49 +++------- automation/release-app-sync.toml | 13 +++ ...redentials-to-the-protected-environment.md | 36 ++++++++ docs/credential-delivery.md | 43 +++++---- mise.toml | 19 ++-- 6 files changed, 128 insertions(+), 121 deletions(-) create mode 100644 automation/release-app-sync.toml create mode 100644 docs/adr/sync-release-app-credentials-to-the-protected-environment.md diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 559038e..d5b075f 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -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: @@ -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 @@ -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 diff --git a/.github/workflows/verify-doppler.yml b/.github/workflows/verify-doppler.yml index 79f2873..650b4c8 100644 --- a/.github/workflows/verify-doppler.yml +++ b/.github/workflows/verify-doppler.yml @@ -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 diff --git a/automation/release-app-sync.toml b/automation/release-app-sync.toml new file mode 100644 index 0000000..d510335 --- /dev/null +++ b/automation/release-app-sync.toml @@ -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" diff --git a/docs/adr/sync-release-app-credentials-to-the-protected-environment.md b/docs/adr/sync-release-app-credentials-to-the-protected-environment.md new file mode 100644 index 0000000..08f55a4 --- /dev/null +++ b/docs/adr/sync-release-app-credentials-to-the-protected-environment.md @@ -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. diff --git a/docs/credential-delivery.md b/docs/credential-delivery.md index 217098a..9ca41eb 100644 --- a/docs/credential-delivery.md +++ b/docs/credential-delivery.md @@ -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. diff --git a/mise.toml b/mise.toml index 53eeaa2..8ffa0ed 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,5 @@ -# Native dev toolchain (mirrors what CI installs). The Rust toolchain itself is -# owned by rust-toolchain.toml. Install with `mise install`, then `just doctor`. +# rust-toolchain.toml selects the Rust compiler. +# Install developer tools with mise, then run just doctor. [tools] # Task runner, git hooks, and the format/lint tools the recipes shell out to. @@ -8,7 +8,7 @@ lefthook = "latest" taplo = "latest" biome = "latest" yamlfmt = "0.21.0" # pinned for format stability -actionlint = "latest" +actionlint = "1.7.12" typos = "latest" jq = "latest" @@ -22,21 +22,14 @@ cargo-binstall = "latest" "cargo:cargo-fuzz" = "latest" "cargo:cargo-insta" = "latest" "cargo:cargo-llvm-cov" = "latest" -# Conventional-Commits linter for the lefthook commit-msg hook — the local guard -# that keeps release tooling fed with well-formed commit messages. cargo backend, -# binstalled. +# Validate commit messages with Conventional Commits. "cargo:committed" = "1" # Pinned (not `latest`): cargo-mutants gates PRs, and its generated mutants and output can shift across releases, so reproducibility beats an implicit floating version. # Review Renovate proposals for this pin deliberately against the mutation suite, as with yamlfmt and libwebp. "cargo:cargo-mutants" = "27.1.0" -# libwebp's `cwebp` / `dwebp` — the conformance oracle used to generate and -# verify golden fixtures. webmproject/libwebp publishes NO GitHub release assets -# (only git tags), so the ubi / github / aqua backends cannot fetch binaries; -# the official prebuilt archives on Google storage are pulled via the `http` -# backend instead. `strip_components = 1` drops the top-level -# `libwebp--/` directory so `bin/` (cwebp, dwebp, ...) is what -# lands on PATH. Verified working on this host (cwebp/dwebp resolve to 1.6.0). +# Official libwebp archives provide the reference executables. +# Drop the archive's top-level directory before exposing bin/. [tools."http:libwebp"] version = "1.6.0" strip_components = 1