Skip to content

Commit 34df442

Browse files
authored
Merge pull request #195 from LerianStudio/develop
fix(release): merge develop into main
2 parents bbb826b + a3fb701 commit 34df442

15 files changed

Lines changed: 679 additions & 80 deletions

File tree

.cursor/rules/reusable-workflows.mdc

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,92 @@ uses: some-action/tool@main # use a specific tag or SHA
417417
- Never print secrets via `echo`, env dumps, or step summaries
418418
- Complex conditional logic belongs in the workflow (not in composites) — full log visibility
419419

420+
### pull_request_target — NEVER checkout fork code
421+
422+
- 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
423+
- 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)
424+
- Prefer `pull_request` trigger over `pull_request_target` unless write permissions to the base repo are explicitly required
425+
- NEVER use `secrets: inherit` in workflows triggered by `pull_request_target` — it exposes all repository secrets to fork code
426+
427+
### Expression injection — sanitize ALL untrusted inputs
428+
429+
NEVER use these directly in `run:` steps:
430+
431+
```yaml
432+
# ❌ All of these are injectable — NEVER interpolate directly in run:
433+
${{ github.event.pull_request.title }}
434+
${{ github.event.pull_request.body }}
435+
${{ github.event.issue.title }}
436+
${{ github.event.issue.body }}
437+
${{ github.event.comment.body }}
438+
${{ github.event.head_commit.message }}
439+
${{ github.event.head_commit.author.name }}
440+
${{ github.event.commits[*].message }}
441+
${{ github.event.discussion.title }}
442+
${{ github.event.discussion.body }}
443+
${{ github.event.review.body }}
444+
${{ github.head_ref }}
445+
${{ github.event.pages[*].page_name }}
446+
```
447+
448+
If any of these values are needed in a `run:` step, pass them through an environment variable:
449+
450+
```yaml
451+
# ✅ Safe — shell variable, not template injection
452+
env:
453+
PR_TITLE: ${{ github.event.pull_request.title }}
454+
run: echo "$PR_TITLE"
455+
```
456+
457+
### workflow_run — treat artifacts as untrusted
458+
459+
- Workflows triggered by `workflow_run` MUST NOT trust artifacts from the triggering workflow blindly
460+
- Validate/sanitize any data extracted from artifacts before use in shell commands or API calls
461+
- Never extract and execute scripts from artifacts without verification
462+
463+
### Permissions — principle of least privilege
464+
465+
- ALWAYS declare explicit `permissions:` block at workflow level
466+
- Default to `contents: read` — only escalate per-job when needed
467+
- NEVER use `permissions: write-all` or leave permissions undeclared (defaults to broad access in public repos)
468+
- For comment/label-only workflows: `permissions: { pull-requests: write }` — nothing else
469+
470+
```yaml
471+
# ✅ Explicit, minimal permissions
472+
permissions:
473+
contents: read
474+
475+
jobs:
476+
comment:
477+
permissions:
478+
pull-requests: write # escalated only for this job
479+
```
480+
481+
### Secrets in fork contexts
482+
483+
- NEVER pass secrets to steps that run fork code
484+
- `pull_request` from a fork does NOT have access to secrets by default — do not circumvent this
485+
- If a workflow needs secrets + fork code, split into two workflows:
486+
- `pull_request` (no secrets, runs fork code)
487+
- `workflow_run` (has secrets, runs trusted code only)
488+
489+
### Script injection via labels/branches
490+
491+
- Validate branch names and label names before using in shell commands
492+
- Branch names can contain shell metacharacters — always quote variables
493+
494+
```yaml
495+
# ✅ Always quote branch/label variables
496+
run: echo "$BRANCH_NAME"
497+
env:
498+
BRANCH_NAME: ${{ github.head_ref }}
499+
```
500+
501+
### Self-hosted runners
502+
503+
- NEVER use self-hosted runners for `pull_request` or `pull_request_target` from public repos — a fork can execute arbitrary code on the runner
504+
- Self-hosted runners are only safe for `push`, `workflow_dispatch`, `schedule`, and other non-fork triggers
505+
420506
### Reserved names — never use as custom secret or input names
421507

422508
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:

.github/workflows/build.yml

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,24 @@ on:
122122
description: 'Force multi-platform build (amd64+arm64) even for beta/rc tags'
123123
type: boolean
124124
default: false
125+
docker_build_args:
126+
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.'
127+
type: string
128+
required: false
129+
default: ''
125130
build_context_from_working_dir:
126131
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).'
127132
type: boolean
128133
default: false
134+
enable_cosign_sign:
135+
description: 'Sign container images with cosign keyless (OIDC) signing after push. Requires id-token: write permission in the caller.'
136+
type: boolean
137+
default: true
129138

130139
permissions:
131140
contents: read
132141
packages: write
142+
id-token: write
133143

134144
jobs:
135145
prepare:
@@ -283,6 +293,7 @@ jobs:
283293
type=semver,pattern={{major}},value=${{ steps.version.outputs.version }},enable=${{ needs.prepare.outputs.is_release }}
284294
285295
- name: Build and push Docker image
296+
id: build-push
286297
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
287298
with:
288299
context: ${{ inputs.build_context_from_working_dir == true && matrix.app.working_dir || inputs.build_context }}
@@ -291,14 +302,50 @@ jobs:
291302
push: true
292303
tags: ${{ steps.meta.outputs.tags }}
293304
labels: ${{ steps.meta.outputs.labels }}
305+
build-args: ${{ inputs.docker_build_args }}
294306
sbom: generator=docker/scout-sbom-indexer:latest
295307
provenance: mode=max
296308
cache-from: type=gha
297309
cache-to: type=gha,mode=max
298310
secrets: |
299311
github_token=${{ secrets.MANAGE_TOKEN }}
300312
301-
# GitOps artifacts for downstream gitops-update workflow
313+
# ----------------- Cosign Image Signing -----------------
314+
- name: Build cosign image references
315+
if: inputs.enable_cosign_sign
316+
id: cosign-refs
317+
env:
318+
DIGEST: ${{ steps.build-push.outputs.digest }}
319+
ENABLE_DOCKERHUB: ${{ inputs.enable_dockerhub }}
320+
ENABLE_GHCR: ${{ inputs.enable_ghcr }}
321+
DOCKERHUB_ORG: ${{ inputs.dockerhub_org }}
322+
APP_NAME: ${{ matrix.app.name }}
323+
GHCR_ORG: ${{ steps.normalize.outputs.owner_lower }}
324+
run: |
325+
REFS=""
326+
327+
if [ "$ENABLE_DOCKERHUB" == "true" ]; then
328+
REFS="${DOCKERHUB_ORG}/${APP_NAME}@${DIGEST}"
329+
fi
330+
331+
if [ "$ENABLE_GHCR" == "true" ]; then
332+
[ -n "$REFS" ] && REFS="${REFS}"$'\n'
333+
REFS="${REFS}ghcr.io/${GHCR_ORG}/${APP_NAME}@${DIGEST}"
334+
fi
335+
336+
{
337+
echo "refs<<EOF"
338+
echo "$REFS"
339+
echo "EOF"
340+
} >> "$GITHUB_OUTPUT"
341+
342+
- name: Sign container images with cosign
343+
if: inputs.enable_cosign_sign
344+
uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign
345+
with:
346+
image-refs: ${{ steps.cosign-refs.outputs.refs }}
347+
348+
# ----------------- GitOps Artifacts -----------------
302349
- name: Create GitOps tag artifact
303350
if: inputs.enable_gitops_artifacts
304351
run: |

.github/workflows/go-release.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,15 @@ on:
6767
description: 'Enable release notifications'
6868
type: boolean
6969
default: false
70+
enable_cosign_sign:
71+
description: 'Sign container images with cosign keyless (OIDC) signing after push. Requires id-token: write permission in the caller.'
72+
type: boolean
73+
default: true
7074

7175
permissions:
7276
contents: write
7377
packages: write
78+
id-token: write
7479

7580
jobs:
7681
release:
@@ -164,6 +169,7 @@ jobs:
164169
tags: ${{ inputs.docker_tags }}
165170

166171
- name: Build and push
172+
id: build-push
167173
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
168174
with:
169175
context: .
@@ -174,6 +180,24 @@ jobs:
174180
cache-from: type=gha
175181
cache-to: type=gha,mode=max
176182

183+
# ----------------- Cosign Image Signing -----------------
184+
- name: Build cosign image references
185+
if: inputs.enable_cosign_sign
186+
id: cosign-refs
187+
env:
188+
DIGEST: ${{ steps.build-push.outputs.digest }}
189+
DOCKER_REGISTRY: ${{ inputs.docker_registry }}
190+
REPOSITORY: ${{ github.repository }}
191+
run: |
192+
REPO=$(echo "$REPOSITORY" | tr '[:upper:]' '[:lower:]')
193+
echo "refs=${DOCKER_REGISTRY}/${REPO}@${DIGEST}" >> "$GITHUB_OUTPUT"
194+
195+
- name: Sign container images with cosign
196+
if: inputs.enable_cosign_sign
197+
uses: LerianStudio/github-actions-shared-workflows/src/security/cosign-sign@feat/cosign-sign
198+
with:
199+
image-refs: ${{ steps.cosign-refs.outputs.refs }}
200+
177201
# Slack notification
178202
notify:
179203
name: Notify

0 commit comments

Comments
 (0)