Skip to content

feat: per-callback job permissions #35

Description

@joshua-temple

Problem

Cascade emits permissions: contents: write at the workflow level for orchestrate (and contents: write, actions: write for promote, contents: write for external-update). Because GHA caps called-workflow permissions to the caller's permissions, every callback job inherits at least contents: write even when the callback only needs contents: read or no permissions at all.

GHA supports permissions: on individual jobs.<id>.uses entries, scoping the token permissions to what that specific job actually needs. Cascade cannot express this today.

The OIDC work (planned separately for id-token) already calls for a permissions field per callback. This issue generalises that to the full permissions map so both that work and least-privilege callbacks can be addressed with one schema field.

What to add

Add an optional permissions: map to each callback entry in the manifest schema:

callbacks:
  - name: build-image
    workflow: ./.github/workflows/build.yaml
    permissions:
      contents: read
      packages: write
  - name: deploy-prod
    workflow: ./.github/workflows/deploy.yaml
    permissions:
      contents: read
      id-token: write   # OIDC

When permissions: is present, the generator emits it on the corresponding jobs.<id> block. When absent, the workflow-level permissions apply as today (backward-compatible).

Valid permission values follow the GHA spec (read, write, none). The generator should validate that all keys are known GHA permission scopes.

Acceptance criteria

  • Manifest schema accepts permissions: map on callback entries (keys: GHA permission scopes, values: read/write/none).
  • Generator emits permissions: on jobs.<id> when the callback entry specifies it.
  • Unknown permission scope keys produce a cascade validate error.
  • Callbacks without permissions: emit no job-level permissions block (unchanged from today).
  • The OIDC id-token: write use case works via this field without any separate schema addition.
  • e2e scenario: a manifest with one callback that has permissions: {contents: read} and one with no permissions: generates a workflow where the first job has permissions: {contents: read} and the second has no job-level permissions: block; cascade validate passes.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions