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
4 changes: 2 additions & 2 deletions .github/workflows/self-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
contents: read
with:
zizmor-config: zizmor.yml
paths: .github
actionlint-paths: .github/workflows
paths: .github templates/workflows
actionlint-paths: .github/workflows templates/workflows

passed:
name: Status checks passed
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/workflow-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
type: string
default: 1.30.1
zizmor-config:
description: zizmor config file. Defaults to .github/zizmor.yml when it exists.
description: zizmor config file. Defaults to .github/zizmor.yml when it exists, else the shared policy (templates/zizmor.yml).
type: string
default: ""
zizmor-min-severity:
Expand Down Expand Up @@ -75,9 +75,22 @@ jobs:
id: cfg
env:
CONFIG: ${{ inputs.zizmor-config }}
# Without a config of its own, a repository gets the shared policy, so
# the @v1 references from the workflow templates pass out of the box.
run: |
cfg="$CONFIG"
[ -z "$cfg" ] && [ -f .github/zizmor.yml ] && cfg=.github/zizmor.yml
if [ -z "$cfg" ]; then
cfg="$RUNNER_TEMP/zizmor.yml"
cat > "$cfg" <<'EOF'
rules:
unpinned-uses:
config:
policies:
privacybydesign/*: ref-pin
"*": hash-pin
EOF
fi
echo "config=$cfg" >> "$GITHUB_OUTPUT"

- name: zizmor
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v1.1.3

- `templates/workflows/`: caller templates for new repositories (Status checks for Go and Node, Delivery, Image rescan, Terraform, releases and more), linted by Self CI. They replace the workflow templates for the org `.github` repository, which public repositories could not use while that repository is private.
- `workflow-lint.yml`: a repository without its own `.github/zizmor.yml` now gets the shared policy (`privacybydesign/*` by tag, everything else by SHA) instead of zizmor's defaults, which reject `@v1`. Repositories set up from the workflow templates pass without copying `zizmor.yml`.

## v1.1.2

- `docker-publish.yml`: the provenance attestation no longer tries to create an artifact storage record. That needed `artifact-metadata: write` from every caller and only produced a warning; the attestation itself is unchanged.
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ jobs:
contents: read
```

Copy [`templates/dependabot.yml`](templates/dependabot.yml) and [`templates/zizmor.yml`](templates/zizmor.yml) to your `.github/` directory: Dependabot keeps your third-party actions pinned and current, and zizmor allows `@v1` for these workflows but requires a commit SHA for every other action.
Copy [`templates/dependabot.yml`](templates/dependabot.yml) to your `.github/` directory: Dependabot keeps your third-party actions pinned and current. workflow-lint allows `@v1` for these workflows and requires a commit SHA for every other action; add [`templates/zizmor.yml`](templates/zizmor.yml) as `.github/zizmor.yml` only if you need to change that policy.

### Starting a new repository

In the privacybydesign organization, a pull request can only merge once a check named `Status checks passed` succeeds. The templates in [`templates/workflows/`](templates/workflows) give you that check:

1. Copy `status-checks-go.yml` or `status-checks-node.yml` to `.github/workflows/status-checks.yml` and adjust the inputs (working directory, Dockerfile, languages). It ends in the `Status checks passed` job.
2. Copy `templates/dependabot.yml` to `.github/dependabot.yml` and remove the ecosystems you don't use.
3. If the repository publishes a container image, also add `delivery.yml` and `image-rescan.yml`.

Open a pull request with these files. It runs the new status checks itself, so it can merge once they pass. Other templates: `release-please.yml`, `go-release.yml`, `npm-publish.yml`, `pr-title.yml`, `terraform.yml`, `ghcr-cleanup.yml` and `scorecard.yml`.

The inputs of each workflow are documented in its file, under `on.workflow_call.inputs`. A reusable workflow gets no more permissions than the calling job grants, so grant the ones its file lists.

Expand Down
29 changes: 29 additions & 0 deletions templates/workflows/delivery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Build, scan (Anchore) and publish the container image to ghcr.io/${{ github.repository }}.
# push to the default branch -> :edge release published / v* tag -> :<version> manual run -> :<branch>
# PR builds are scanned in "Status checks" (push-image: false); published
# images are re-scanned weekly by "Image rescan".
name: Delivery

on:
push:
branches: [main] # your default branch
release:
types: [published]
workflow_dispatch:

permissions: {}

jobs:
docker:
uses: privacybydesign/github-workflows/.github/workflows/docker-publish.yml@v1
permissions:
contents: read
packages: write
security-events: write
id-token: write
attestations: write
with:
platforms: linux/amd64,linux/arm64
# tag-latest: true
# tag-semver-rollups: true
# cosign: true # keyless signature; recorded in the public Rekor log
22 changes: 22 additions & 0 deletions templates/workflows/ghcr-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Weekly GHCR retention: delete unreferenced untagged versions (old edge builds).
# Starts as a dry run: check the job summary, then set dry-run: false.
name: GHCR clean-up

on:
schedule:
- cron: "41 3 * * 0" # Sundays
workflow_dispatch:

permissions: {}

jobs:
cleanup:
uses: privacybydesign/github-workflows/.github/workflows/ghcr-cleanup.yml@v1
permissions:
contents: read
packages: write
with:
dry-run: true
min-age-days: 14
# delete-tags-regex: 'feature-.*|fix-.*' # branch tags from manual runs; never match edge or versions
# tag-max-age-days: 90
28 changes: 28 additions & 0 deletions templates/workflows/go-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Build Go binaries for a tag, attach them (+ SBOM, checksums, attestations)
# to a DRAFT release and publish it. Works with immutable releases.
# With release-please: set "draft": true and "force-tag-creation": true.
name: Release binaries

on:
push:
tags: ["v*"]
pull_request: # smoke-test the build matrix, nothing is uploaded

permissions: {}

jobs:
binaries:
uses: privacybydesign/github-workflows/.github/workflows/go-release.yml@v1
permissions:
contents: write
id-token: write
attestations: write
with:
binary-name: CHANGE_ME
main-package: ./cmd/CHANGE_ME
# version-variable: main.version
# require-changelog-entry: true
secrets:
# Publishing with the App makes 'release: published' (Delivery) run.
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
20 changes: 20 additions & 0 deletions templates/workflows/image-rescan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Weekly re-scan of the PUBLISHED images (all platforms) for new CVEs.
name: Image rescan

on:
schedule:
- cron: "23 6 * * 1" # Mondays
workflow_dispatch:

permissions: {}

jobs:
rescan:
uses: privacybydesign/github-workflows/.github/workflows/image-rescan.yml@v1
permissions:
contents: read
packages: read
security-events: write
with:
tags: edge latest-release
severity-cutoff: high
21 changes: 21 additions & 0 deletions templates/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Publish to npm when a GitHub release is published.
# One-time setup on npmjs.com: package > Settings > Trusted Publisher >
# GitHub Actions, owner privacybydesign, this repository, workflow file = THIS file's name.
name: Publish to npm

on:
release:
types: [published]

permissions: {}

jobs:
npm:
uses: privacybydesign/github-workflows/.github/workflows/npm-publish.yml@v1
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
# with:
# release-command: npm run release # semantic-release instead of tag-driven
13 changes: 13 additions & 0 deletions templates/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: PR title

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions: {}

jobs:
pr-title:
uses: privacybydesign/github-workflows/.github/workflows/pr-title.yml@v1
permissions:
pull-requests: read
18 changes: 18 additions & 0 deletions templates/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Release please

on:
push:
branches: [main] # your default branch

permissions: {}

jobs:
release-please:
uses: privacybydesign/github-workflows/.github/workflows/release-please.yml@v1
permissions:
contents: write
pull-requests: write
issues: write
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
37 changes: 37 additions & 0 deletions templates/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# OpenSSF Scorecard (public repositories). Kept standalone on purpose:
# Scorecard only publishes results from a workflow that contains just this job.
name: Scorecard

on:
branch_protection_rule:
schedule:
- cron: "37 5 * * 1"
push:
branches: [main] # your default branch
workflow_dispatch:

permissions: {}

jobs:
analysis:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
actions: read
security-events: write
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
with:
results_file: results.sarif
results_format: sarif
publish_results: true

- uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
sarif_file: results.sarif
73 changes: 73 additions & 0 deletions templates/workflows/status-checks-go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Status checks for a Go service/library (optionally with a Dockerfile).
name: Status checks

on:
push:
branches: [main] # your default branch
pull_request:
merge_group:
schedule:
- cron: "17 6 * * 1" # Mondays: CodeQL with fresh queries
workflow_dispatch:

permissions: {}

jobs:
go:
uses: privacybydesign/github-workflows/.github/workflows/go-checks.yml@v1
permissions:
contents: read
with:
working-directory: .
# test-dockerfile: Dockerfile.test
# apt-packages: libpcsclite-dev

# Build + scan the image on PRs without pushing. Remove if there is no Dockerfile.
docker:
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
uses: privacybydesign/github-workflows/.github/workflows/docker-publish.yml@v1
permissions:
contents: read
packages: write
security-events: write
id-token: write
attestations: write
with:
push-image: false

codeql:
uses: privacybydesign/github-workflows/.github/workflows/codeql.yml@v1
permissions:
contents: read
security-events: write
actions: read
with:
languages: '["go", "actions"]'

dependency-review:
uses: privacybydesign/github-workflows/.github/workflows/dependency-review.yml@v1
permissions:
contents: read
pull-requests: write

workflow-lint:
uses: privacybydesign/github-workflows/.github/workflows/workflow-lint.yml@v1
permissions:
contents: read

# Single required check for rulesets/branch protection: job names inside this
# workflow can change without touching the ruleset.
passed:
name: Status checks passed
if: always()
needs: [go, docker, codeql, dependency-review, workflow-lint]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: All required jobs succeeded
env:
RESULTS: ${{ toJSON(needs.*.result) }}
run: |
echo "$RESULTS"
if grep -qE '"(failure|cancelled)"' <<< "$RESULTS"; then exit 1; fi
Loading
Loading