Skip to content

feat: Composite action for synchronizing SDK snippets#82

Merged
kinyoklion merged 5 commits intomainfrom
rlamb/sync-snippets-action
May 1, 2026
Merged

feat: Composite action for synchronizing SDK snippets#82
kinyoklion merged 5 commits intomainfrom
rlamb/sync-snippets-action

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

@kinyoklion kinyoklion commented Apr 29, 2026

Summary

New composite action at actions/sync-snippets/. Consumers call it via workflow_dispatch (or a daily cron) to stay current with snippet changes published by launchdarkly/sdk-meta.

on:
  workflow_dispatch:
  schedule: [ { cron: '0 12 * * *' } ]

permissions:
  contents: write
  pull-requests: write

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: launchdarkly/gh-actions/actions/sync-snippets@main
        with:
          entrypoints: |
            static/ld/components/getStarted
          github-token: ${{ secrets.GITHUB_TOKEN }}

How it works

  1. Resolves the latest snippets/v* GitHub Release on launchdarkly/sdk-meta (override with version:).
  2. Downloads the platform-specific archive (snippets_<version>_<os>_<arch>.tar.gz).
  3. Verifies the SLSA build-provenance attestation via gh attestation verify --signer-workflow launchdarkly/sdk-meta/.github/workflows/release-please.yml. Sigstore-backed keyless OIDC under the hood, but no cosign install required — gh ships preinstalled on every GitHub-hosted runner.
  4. Runs snippets render --target=<adapter> --entrypoint=<dir>... with one --entrypoint= flag per non-empty line of the entrypoints: input. The CLI walks each directory, picks up files containing the SDK_SNIPPET:RENDER: sentinel, and rewrites their marked regions in place. The binary embeds the canonical sdks/ tree at build time, so there is nothing else to fetch.
  5. Opens or updates a sync PR via peter-evans/create-pull-request when the render produced any diff. Empty diff → exit 0, no PR.

Inputs / outputs

See actions/sync-snippets/README.md. Required: entrypoints, github-token. Everything else has a sensible default.

How the supply chain is locked down

  • Signer pinned to a specific workflow path. gh attestation verify --signer-workflow launchdarkly/sdk-meta/.github/workflows/release-please.yml rejects any artifact whose attestation was issued by a different workflow file. A token leaked from any other workflow in any other repo cannot produce a matching OIDC claim.
  • No long-lived signing keys. Each release issues its own attestation via the GitHub-hosted Sigstore Fulcio CA against the workflow's OIDC token. Nothing to rotate, store, or accidentally check in.
  • Snippet sources travel with the binary. The CLI's --sdks= flag is optional; when omitted (the default for this action) it reads from embed.FS. Pinning a release version pins both the engine and the snippet content atomically — no separate sdks/ checkout step.

Depends on

End-to-end validation

Wired up in a private sandbox repo (launchdarkly/learn-release-please) via:

  • snippet-test/getStarted.tsx — TSX file with two SDK_SNIPPET:RENDER: markers (python-server-sdk/getting-started/mkdir with no inputs, plus .../install with one optional version prop) seeded with hash=0 version=0.0.0 placeholders.
  • .github/workflows/sync-snippets.ymlworkflow_dispatch referencing this action via @rlamb/sync-snippets-action.

Sandbox PR #89 merged the harness; the next dispatch (run 25190361399) ran green and the action opened sandbox PR #91, rewriting both markers with their v0.2.1 content. Both render modes were exercised — bare-JSX-text (no inputs) and template-literal-with-ternary (optional input → ${version ? \==${version}` : ''}`).

Test plan

  • Sandbox harness in learn-release-please opens a PR with rewritten markers (run #25190361399 → PR #91).
  • Both renderer paths exercised (bare text + template-literal-with-conditional).
  • gh attestation verify against the published v0.2.1 archive succeeds with the --signer-workflow pin.
  • Re-dispatch on the sandbox after peter-evans/create-pull-request already opened a PR — expect: same branch updated, no second PR opened.
  • Hand-edit one rendered region in the sandbox before re-running — expect: the action opens a PR that reverts the hand-edit (the hash mismatch is what snippets verify flags; render just re-renders).
  • Pin version: to a non-existent tag — expect: clean failure at the resolve step.

Known sharp edges

  • ${{ ... }} in the action manifest's description: field is template-evaluated at action-load time. Don't put example workflow snippets containing secrets.* into description: — those parse-fail with Unrecognized named-value: 'secrets'. Fixed in a0ad8bb by moving the inline example to README.md.
  • Consumer repos must enable Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests" for peter-evans/create-pull-request to open the sync PR; otherwise the PR-open step is silently blocked by the default GITHUB_TOKEN policy.

New action at actions/sync-snippets/. Resolves the latest
launchdarkly/sdk-meta `snippets/*` GitHub Release, downloads the
platform's signed binary, cosign-verifies it (keyless / OIDC,
identity pinned to sdk-meta's release-please workflow), runs
`snippets render` against the consumer checkout, and opens or
updates a sync PR via peter-evans/create-pull-request when the
render produced any diff.

Designed for gonfalon (and future ld-docs-private use); both call
this with `@main`. Daily cron + workflow_dispatch on the consumer
side keeps the consumer current without a coordinated rollout.

Implementation notes:

- The `snippets` CLI binary embeds the canonical sdks/ tree at build
  time (sdk-meta side, separate PR). One artifact, one signature
  check, atomic content + engine pinning.
- `--certificate-identity-regexp` matches sdk-meta's release-please
  workflow path on main, so a token leaked from any other workflow
  cannot produce a matching OIDC claim.
- Per-platform archive name resolution mirrors goreleaser's default
  template (snippets_<version>_<os>_<arch>.tar.gz).
The CLI no longer asks each sdk.yaml which file in the consumer to
rewrite (see launchdarkly/sdk-meta#405); instead the consumer declares
its own list of search roots. Plumb that through:

- New required input `entrypoints` (newline-separated list of
  directories, relative to $GITHUB_WORKSPACE).
- Render step parses the input into individual `--entrypoint=` flags
  and passes them to `snippets render`.
- Empty / whitespace-only lines are tolerated and skipped; an empty
  list fails loudly.
- README + the in-action usage block updated to show the new shape.

Consumer wiring becomes:

    - uses: launchdarkly/gh-actions/actions/sync-snippets@main
      with:
        entrypoints: |
          static/ld/components/getStarted
        github-token: ${{ secrets.GITHUB_TOKEN }}
The upstream sdk-meta release pipeline is moving from cosign keyless
sidecars to GitHub's first-party SLSA build-provenance attestation
(launchdarkly/sdk-meta#409). Match it on the verifier side:

- Replace `cosign verify-blob` with `gh attestation verify --signer-
  workflow launchdarkly/sdk-meta/.github/workflows/release-please.yml`.
  Same identity pinning, but the attestation is fetched from sdk-meta's
  repo-level attestation store rather than as `.sig`/`.pem` release
  assets.
- Drop the `sigstore/cosign-installer` step. `gh` ships preinstalled on
  every GitHub-hosted runner.
- Drop the `id-token: write` permission from the consumer wiring
  example — verification is read-only against sdk-meta's attestation
  store and doesn't need an OIDC token.
- Update README copy to match.
The action manifest's description: field is template-evaluated at action-load
time. Embedding a copy-paste-able workflow snippet that contained
${{ secrets.GITHUB_TOKEN }} caused 'Unrecognized named-value: secrets'
parse failures whenever a consumer used the action — secrets aren't in
scope when the manifest itself is being loaded.

Replace the inline workflow example with a prose pointer to README.md,
which carries the full consumer wiring snippet.
@kinyoklion kinyoklion changed the title feat(sync-snippets): composite action that pulls SDK snippets feat: Composite action for synchronizing SDK snippets Apr 30, 2026
@kinyoklion kinyoklion marked this pull request as ready for review April 30, 2026 21:41
Comment thread actions/sync-snippets/action.yml
…ution

A 'latest' lookup that catches sdk-meta's release-pipeline mid-publish
window would point at a draft release whose archives and attestations
haven't uploaded yet — and the subsequent 'gh release download' /
'gh attestation verify' would fail noisily but pointlessly. Pre-releases
are excluded under the same logic: consumers asking for 'latest' want
the latest stable, not a release-candidate the pipeline cut for testing.

Per @keelerm84 review on PR #82.
@kinyoklion kinyoklion merged commit 84fb025 into main May 1, 2026
2 checks passed
@kinyoklion kinyoklion deleted the rlamb/sync-snippets-action branch May 1, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants