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
86 changes: 86 additions & 0 deletions .cursor/rules/reusable-workflows.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,92 @@ uses: some-action/tool@main # use a specific tag or SHA
- Never print secrets via `echo`, env dumps, or step summaries
- Complex conditional logic belongs in the workflow (not in composites) — full log visibility

### pull_request_target — NEVER checkout fork code

- NEVER use `actions/checkout` with `ref: ${{ github.event.pull_request.head.ref }}` or `ref: ${{ github.event.pull_request.head.sha }}` in `pull_request_target` workflows
- If `pull_request_target` is needed (e.g., labeling, commenting), it MUST NOT run any code from the fork (no build, no test, no script execution from the PR branch)
- Prefer `pull_request` trigger over `pull_request_target` unless write permissions to the base repo are explicitly required
- NEVER use `secrets: inherit` in workflows triggered by `pull_request_target` — it exposes all repository secrets to fork code

### Expression injection — sanitize ALL untrusted inputs

NEVER use these directly in `run:` steps:

```yaml
# ❌ All of these are injectable — NEVER interpolate directly in run:
${{ github.event.pull_request.title }}
${{ github.event.pull_request.body }}
${{ github.event.issue.title }}
${{ github.event.issue.body }}
${{ github.event.comment.body }}
${{ github.event.head_commit.message }}
${{ github.event.head_commit.author.name }}
${{ github.event.commits[*].message }}
${{ github.event.discussion.title }}
${{ github.event.discussion.body }}
${{ github.event.review.body }}
${{ github.head_ref }}
${{ github.event.pages[*].page_name }}
```

If any of these values are needed in a `run:` step, pass them through an environment variable:

```yaml
# ✅ Safe — shell variable, not template injection
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE"
```

### workflow_run — treat artifacts as untrusted

- Workflows triggered by `workflow_run` MUST NOT trust artifacts from the triggering workflow blindly
- Validate/sanitize any data extracted from artifacts before use in shell commands or API calls
- Never extract and execute scripts from artifacts without verification

### Permissions — principle of least privilege

- ALWAYS declare explicit `permissions:` block at workflow level
- Default to `contents: read` — only escalate per-job when needed
- NEVER use `permissions: write-all` or leave permissions undeclared (defaults to broad access in public repos)
- For comment/label-only workflows: `permissions: { pull-requests: write }` — nothing else

```yaml
# ✅ Explicit, minimal permissions
permissions:
contents: read

jobs:
comment:
permissions:
pull-requests: write # escalated only for this job
```

### Secrets in fork contexts

- NEVER pass secrets to steps that run fork code
- `pull_request` from a fork does NOT have access to secrets by default — do not circumvent this
- If a workflow needs secrets + fork code, split into two workflows:
- `pull_request` (no secrets, runs fork code)
- `workflow_run` (has secrets, runs trusted code only)

### Script injection via labels/branches

- Validate branch names and label names before using in shell commands
- Branch names can contain shell metacharacters — always quote variables

```yaml
# ✅ Always quote branch/label variables
run: echo "$BRANCH_NAME"
env:
BRANCH_NAME: ${{ github.head_ref }}
```

### Self-hosted runners

- NEVER use self-hosted runners for `pull_request` or `pull_request_target` from public repos — a fork can execute arbitrary code on the runner
- Self-hosted runners are only safe for `push`, `workflow_dispatch`, `schedule`, and other non-fork triggers

Comment on lines +420 to +505
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reconcile the workflow_dispatch rule in this document.

This rules refresh lands in a file that still tells agents reusable workflows must support workflow_dispatch. Current repo policy for externally consumed reusable workflows is the opposite, so the document now gives conflicting instructions and will drive incorrect edits.

Based on learnings, "reusable workflows under .github/workflows must NOT include a workflow_dispatch trigger."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.cursor/rules/reusable-workflows.mdc around lines 420 - 505, Update the
contradictory guidance about workflow_dispatch: replace any statement that
"reusable workflows must support workflow_dispatch" with the current repo policy
that "reusable workflows under .github/workflows must NOT include a
workflow_dispatch trigger", and edit adjacent examples and bullet points to
reflect this (search for occurrences of "workflow_dispatch" and the phrase
"support `workflow_dispatch`" in this document). Ensure the section that lists
accepted triggers for reusable workflows and any examples are reconciled so they
no longer recommend adding `workflow_dispatch` to reusable workflows and instead
explain splitting into separate workflows if interactive/manual dispatch is
required.

### Reserved names — never use as custom secret or input names

GitHub reserves the `GITHUB_*` prefix for all built-in variables and the `ACTIONS_*` prefix for runner internals. Declaring a custom secret or input with these names causes the job to fail silently or be ignored:
Expand Down
49 changes: 48 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,24 @@
description: 'Force multi-platform build (amd64+arm64) even for beta/rc tags'
type: boolean
default: false
docker_build_args:
description: 'Newline-separated Docker build arguments to pass to docker build (e.g., "APP_NAME=spi\nCOMPONENT_NAME=api"). Forwarded to docker/build-push-action build-args.'
type: string
required: false
default: ''
build_context_from_working_dir:
description: 'Use the component working_dir as Docker build context instead of build_context. Useful for independent modules (e.g., tools with their own go.mod).'
type: boolean
default: false
enable_cosign_sign:
description: 'Sign container images with cosign keyless (OIDC) signing after push. Requires id-token: write permission in the caller.'
type: boolean
default: true

permissions:
contents: read
packages: write
id-token: write
Comment on lines 139 to +142
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restrict id-token: write to the build job.

The new signing path only runs in build, but workflow-scope permissions also hand OIDC access to prepare, notify, and dispatch-helm.

As per coding guidelines, "Always declare explicit least-privilege permissions: (default contents: read, avoid broad write-all)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yml around lines 139 - 142, Top-level workflow
permissions currently grant id-token: write to all jobs; remove id-token: write
from the global permissions block and instead add a job-scoped permissions entry
under the build job (jobs.build.permissions) that sets id-token: write so only
the build job gets OIDC access; leave global permissions.contents: read and
packages: write as appropriate and ensure other jobs (prepare, notify,
dispatch-helm) do not receive id-token write privileges.


jobs:
prepare:
Expand Down Expand Up @@ -283,6 +293,7 @@
type=semver,pattern={{major}},value=${{ steps.version.outputs.version }},enable=${{ needs.prepare.outputs.is_release }}

- name: Build and push Docker image
id: build-push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: ${{ inputs.build_context_from_working_dir == true && matrix.app.working_dir || inputs.build_context }}
Expand All @@ -291,14 +302,50 @@
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: ${{ inputs.docker_build_args }}
sbom: generator=docker/scout-sbom-indexer:latest
provenance: mode=max
cache-from: type=gha
cache-to: type=gha,mode=max
secrets: |
github_token=${{ secrets.MANAGE_TOKEN }}

# GitOps artifacts for downstream gitops-update workflow
# ----------------- Cosign Image Signing -----------------
- name: Build cosign image references
if: inputs.enable_cosign_sign
id: cosign-refs
env:
DIGEST: ${{ steps.build-push.outputs.digest }}
ENABLE_DOCKERHUB: ${{ inputs.enable_dockerhub }}
ENABLE_GHCR: ${{ inputs.enable_ghcr }}
DOCKERHUB_ORG: ${{ inputs.dockerhub_org }}
APP_NAME: ${{ matrix.app.name }}
GHCR_ORG: ${{ steps.normalize.outputs.owner_lower }}
run: |
REFS=""

if [ "$ENABLE_DOCKERHUB" == "true" ]; then
REFS="${DOCKERHUB_ORG}/${APP_NAME}@${DIGEST}"
fi

if [ "$ENABLE_GHCR" == "true" ]; then
[ -n "$REFS" ] && REFS="${REFS}"$'\n'
REFS="${REFS}ghcr.io/${GHCR_ORG}/${APP_NAME}@${DIGEST}"
fi

{
echo "refs<<EOF"
echo "$REFS"
echo "EOF"
} >> "$GITHUB_OUTPUT"
Comment on lines +314 to +340
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Custom ghcr_org is ignored when building cosign refs.

Set image names resolves inputs.ghcr_org, but this block always signs ghcr.io/${{ steps.normalize.outputs.owner_lower }}/${APP_NAME}. When callers override ghcr_org, cosign targets a different repository than the image that was pushed.

Suggested fix
       - name: Build cosign image references
         if: inputs.enable_cosign_sign
         id: cosign-refs
         env:
           DIGEST: ${{ steps.build-push.outputs.digest }}
           ENABLE_DOCKERHUB: ${{ inputs.enable_dockerhub }}
           ENABLE_GHCR: ${{ inputs.enable_ghcr }}
           DOCKERHUB_ORG: ${{ inputs.dockerhub_org }}
           APP_NAME: ${{ matrix.app.name }}
-          GHCR_ORG: ${{ steps.normalize.outputs.owner_lower }}
+          INPUT_GHCR_ORG: ${{ inputs.ghcr_org }}
+          DEFAULT_GHCR_ORG: ${{ steps.normalize.outputs.owner_lower }}
         run: |
           REFS=""
+          GHCR_ORG="$INPUT_GHCR_ORG"
+          if [ -z "$GHCR_ORG" ]; then
+            GHCR_ORG="$DEFAULT_GHCR_ORG"
+          else
+            GHCR_ORG=$(echo "$GHCR_ORG" | tr '[:upper:]' '[:lower:]')
+          fi

           if [ "$ENABLE_DOCKERHUB" == "true" ]; then
             REFS="${DOCKERHUB_ORG}/${APP_NAME}@${DIGEST}"
           fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yml around lines 314 - 340, The cosign-refs step
ignores a caller-provided ghcr_org and always uses
steps.normalize.outputs.owner_lower; update the GHCR_ORG environment binding for
the cosign-refs job to prefer inputs.ghcr_org and fall back to
steps.normalize.outputs.owner_lower (e.g. set GHCR_ORG: ${{ inputs.ghcr_org ||
steps.normalize.outputs.owner_lower }}), and ensure the GHCR_ORG variable is
used unchanged when constructing the ghcr.io/${GHCR_ORG}/${APP_NAME}@${DIGEST}
reference in the run block.


- name: Sign container images with cosign
if: inputs.enable_cosign_sign
uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign

Check warning on line 344 in .github/workflows/build.yml

View workflow job for this annotation

GitHub Actions / Pinned Actions Check

Internal action not pinned to a version: uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign
with:
image-refs: ${{ steps.cosign-refs.outputs.refs }}
Comment on lines +342 to +346
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not publish the workflow with src/security/cosign-sign@feat/cosign-sign.

A caller using a tagged or mainline revision of this reusable workflow would still execute action code from a mutable feature branch. That breaks release immutability and can fail once the branch moves or is deleted.

As per coding guidelines, ".cursor/rules/reusable-workflows.mdc: External callers must use an org-owned ref pinned to a release tag (no @main/@master)."

🧰 Tools
🪛 GitHub Check: Pinned Actions Check

[warning] 344-344:
Internal action not pinned to a version: uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yml around lines 342 - 346, The workflow currently
references a mutable branch in the reusable workflow usage (the uses:
LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign
line); change that to a pinned, org-owned release tag (e.g., `@vX.Y.Z`) for the
reusable workflow so external callers run immutable, release-tagged code—update
the uses reference to the appropriate release tag for
github-actions-shared-workflows and confirm it points to the org-owned
repository tag rather than a branch name.


# ----------------- GitOps Artifacts -----------------
- name: Create GitOps tag artifact
if: inputs.enable_gitops_artifacts
run: |
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/go-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@
description: 'Enable release notifications'
type: boolean
default: false
enable_cosign_sign:
description: 'Sign container images with cosign keyless (OIDC) signing after push. Requires id-token: write permission in the caller.'
type: boolean
default: true

permissions:
contents: write
packages: write
id-token: write
Comment on lines 75 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scope OIDC to the docker job.

Only docker performs keyless signing. Keeping id-token: write at workflow scope also grants it to release, homebrew, and notify, which widens the credential surface for no benefit.

As per coding guidelines, "Always declare explicit least-privilege permissions: (default contents: read, avoid broad write-all)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/go-release.yml around lines 75 - 78, The top-level
permissions block currently grants id-token: write for the whole workflow;
restrict this to only the docker job by removing id-token: write from the global
permissions and adding a permissions: block under the docker job that includes
id-token: write (keep contents/packages as needed per-job), so only the docker
job performs keyless signing with id-token, and other jobs like
release/homebrew/notify retain least-privilege defaults; update the docker job's
permissions section accordingly and remove id-token from the workflow-level
permissions.


jobs:
release:
Expand Down Expand Up @@ -164,6 +169,7 @@
tags: ${{ inputs.docker_tags }}

- name: Build and push
id: build-push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: .
Expand All @@ -174,6 +180,24 @@
cache-from: type=gha
cache-to: type=gha,mode=max

# ----------------- Cosign Image Signing -----------------
- name: Build cosign image references
if: inputs.enable_cosign_sign
id: cosign-refs
env:
DIGEST: ${{ steps.build-push.outputs.digest }}
DOCKER_REGISTRY: ${{ inputs.docker_registry }}
REPOSITORY: ${{ github.repository }}
run: |
REPO=$(echo "$REPOSITORY" | tr '[:upper:]' '[:lower:]')
echo "refs=${DOCKER_REGISTRY}/${REPO}@${DIGEST}" >> "$GITHUB_OUTPUT"

- name: Sign container images with cosign
if: inputs.enable_cosign_sign
uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign

Check warning on line 197 in .github/workflows/go-release.yml

View workflow job for this annotation

GitHub Actions / Pinned Actions Check

Internal action not pinned to a version: uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign
with:
image-refs: ${{ steps.cosign-refs.outputs.refs }}
Comment on lines +195 to +199
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not publish the workflow with src/security/cosign-sign@feat/cosign-sign.

A caller using a tagged or mainline revision of this reusable workflow would still execute action code from a mutable feature branch. That breaks release immutability and can fail once the branch moves or is deleted.

As per coding guidelines, ".cursor/rules/reusable-workflows.mdc: External callers must use an org-owned ref pinned to a release tag (no @main/@master)."

🧰 Tools
🪛 GitHub Check: Pinned Actions Check

[warning] 197-197:
Internal action not pinned to a version: uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/go-release.yml around lines 195 - 199, The reusable
workflow step "Sign container images with cosign" currently uses a mutable
feature branch ref
(LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign);
replace that with an org-owned immutable ref (a release tag or commit SHA) to
satisfy the reusable-workflows policy—e.g., change the uses value to the
corresponding release tag (or pinned SHA) for
LerianStudio/github-actions-shared-workflows so external callers run a fixed,
immutable revision instead of a feature branch.


# Slack notification
notify:
name: Notify
Expand Down
Loading
Loading