Github Actions Automation
Site enrollment is the process of:
-
publishing the WEBCAT artifacts that let the browser extension verify your site; and
-
keeping those artifacts current as your site evolves.
This page focuses on how to use WEBCAT-provided GitHub Actions workflows to
integrate webcat-cli with Sigstore into a static site's CI/CD pipeline without
breaking reproducibility. For prerequisites, such as choosing between Sigstore
and Sigsum, webcat-cli usage, and the webcat.config.json schema, see the
webcat-cli readme.
WEBCAT artifacts
The following files must be served from your site's /.well-known/webcat/ path:
| File | Description |
|---|---|
enrollment.json and enrollment-prev.json | The enrollment information |
manifest.json | The manifest |
bundle.json and bundle-prev.json | The bundle |
All of these files are committed to the source repository. They are updated as part of the WEBCAT-provided GitHub Actions workflows, not regenerated from scratch on every build.
Reproducibility for static sites
WEBCAT's GitHub Actions workflows must be integrated in a way that preserves the site's reproducibility even when these WEBCAT-generated files have changed. Specifically, merging changes to these files:
-
MUST publish the site with these changes included, including rebuilding the site if necessary.
-
MUST NOT cause other files (i.e., outside of
.well-known/webcat/) to change. Version numbers and timestamps MUST remain unchanged. -
MUST NOT trigger a loop of updates to these files.
Common pitfalls include:
Version stamps. If CI stamps a version string (e.g., YYYY.MM.DD.HH.MM.SS)
from the current clock, a rebuild triggered by merging an updated manifest will
produce a different version string than the original build. This changes the
manifest and triggers another cycle. One solution is to derive the version from
the timestamp of the Git commit instead.
File modification times. Static-site generators that use file modification times (mtimes) will produce different output if files are checked out with the current time rather than their committed time. Clamp mtimes to a consistent timestamp (e.g., per version or per commit) across builds.
Workflow architecture
One way to satisfy these reproducibility requirements is to separate concerns across three workflows:
1. Build with WEBCAT
Triggered on push to the main branch, excluding the .well-known/webcat/
path. Builds the site, deploys it, and then calls WEBCAT's reusable workflows to
update the manifest and the bundle.
The paths-ignore exclusion prevents an infinite loop when CI later commits the
updated manifest.
Jobs, in order:
-
Build and deploy: Build the site, deploy it to the CDN, and upload the built output and
webcat.config.jsonas artifacts for the next step. -
Generate manifest: Generate and sign a new
manifest.json(in a new pull request for review). -
Assemble bundle: Combine the manifest and Sigstore bundle into
bundle.json(in a new pull request for review).
2. Publish
Triggered on push to the main branch, only for the .well-known/webcat/
path. Rebuilds and redeploys the site so that the newly committed manifests are
served from the CDN.
This workflow must not upload artifacts or trigger the WEBCAT manifest-generation steps.
3. Enrollment sync
Triggered on a daily schedule (and manually via workflow_dispatch). Fetches
the latest Sigstore trusted-root from the upstream WEBCAT CLI repository and
opens a pull request if it differs from the current enrollment.json.
Merging the resulting pull request triggers the Publish workflow, which redeploys with the updated enrollment files.
HTTP header alignment
The Content-Security-Policy header set by your CDN or HTTP server must exactly
match the default_csp (and any extra_csp entries) in the WEBCAT
configuration.
Initial setup
Before manifests can be generated automatically in CI, the following must be in place:
-
Grant workflow permissions. The CI jobs that open pull requests require write permissions. In GitHub, this is Settings → Actions → General → Workflow permissions → Read and write permissions.
-
Configure WEBCAT. Commit a
webcat.config.jsonwith theappURL,default_csp, and other fields. Push. -
Create
enrollment.json. Run the enrollment-sync workflow manually (via the GitHub Actionsworkflow_dispatchtrigger). Review and merge the pull request it opens. -
Trigger the first build. Push a content change (outside of
.well-known/webcat/) to start the build-with-WEBCAT workflow. The manifest and bundle will follow automatically in new pull requests.
Worked example
The following sections illustrate the approach outlined above for a static site built with ikiwiki and deployed to static hosting. Here we use Cloudflare Pages; the Cloudflare-side configuration is not covered.
Repository layout
.
├── ikiwiki.setup # ikiwiki configuration
├── webcat.config.json # WEBCAT configuration
├── src/ # ikiwiki source (srcdir)
│ ├── .well-known/webcat/ # WEBCAT artifacts (committed from steps 3 and 4 above)
│ │ ├── enrollment.json
│ │ ├── enrollment-prev.json
│ │ ├── bundle.json
│ │ ├── bundle-prev.json
│ │ └── manifest.json
│ ├── _headers # Cloudflare Pages HTTP headers
│ └── … # site content
└── .github/workflows/
├── ikiwiki-with-manifest.yaml # See section: "'Build with WEBCAT' Workflow"
├── build-and-deploy.yaml # See section: "Reusable Build-and-Deploy Job"
├── deploy.yaml # See section: "Publishing Workflow"
└── sync-sigstore-enrollment.yml # See section: "Enrollment-sync Workflow"
ikiwiki writes the built site to dist/. In addition, in ikiwiki.setup,
include: ^\.well-known overrides ikiwiki's default behavior of skipping
dot-directories, so that .well-known/ is included in the built
site.
"Build with WEBCAT" workflow
# .github/workflows/ikiwiki-with-manifest.yaml
name: Update WEBCAT Manifest
on:
push:
branches: [main]
paths-ignore: ["src/.well-known/webcat/**"]
workflow_dispatch:
jobs:
build:
uses: ./.github/workflows/build-and-deploy.yaml
with:
upload_webcat_artifacts: true
secrets: inherit
webcat-manifest:
needs: build
permissions:
contents: write
pull-requests: write
id-token: write
uses: freedomofpress/webcat-cli/.github/workflows/webcat-generate-and-commit-manifest.yml@main
with:
manifest_path: src/.well-known/webcat/manifest.json
webcat-bundle:
needs: webcat-manifest
permissions:
contents: write
uses: freedomofpress/webcat-cli/.github/workflows/webcat-assemble-bundle.yaml@main
with:
enrollment_path: src/.well-known/webcat/enrollment.json
manifest_path: src/.well-known/webcat/manifest.json
bundle_path: src/.well-known/webcat/bundle.json
Reusable build-and-deploy job
# .github/workflows/build-and-deploy.yaml
name: Build and Deploy
on:
workflow_call:
inputs:
upload_webcat_artifacts:
type: boolean
required: false
default: false
env:
DIST: dist
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history needed for --gettime and mtime restoration
- name: Set Git identity
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "<>"
- name: Install ikiwiki and supporting tools
run: |
sudo apt-get update
sudo apt-get install --quiet --yes ikiwiki
- name: Get submodules
run: git submodule update --init
- name: Restore mtimes
run: share/git-tools/git-restore-mtime
- name: Build site
run: ikiwiki --gettime --setup ikiwiki.setup
- name: Duplicate index as error page
working-directory: ${{ env.DIST }}
run: cp index.html 404.html
- name: Stamp version
if: inputs.upload_webcat_artifacts
run: |
VERSION="$(TZ=UTC git log -1 --format=%cd --date=format-local:'%Y.%m.%d.%H.%M.%S')"
jq --arg v "$VERSION" '.version = $v' webcat.config.json > webcat.config.json.tmp
mv webcat.config.json.tmp webcat.config.json
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ${{ env.DIST }} --project-name=${{ vars.CLOUDFLARE_PAGES_PROJECT }}
- name: Upload dist artifact
if: inputs.upload_webcat_artifacts
uses: actions/upload-artifact@v4
with:
name: webcat-dist
path: dist
- name: Upload WEBCAT config
if: inputs.upload_webcat_artifacts
uses: actions/upload-artifact@v4
with:
name: webcat-config
path: webcat.config.json
The version is derived from git log -1 --format=%cd (the commit timestamp)
rather than date -u, so that rebuilding from the same commit always produces
the same version string.
As discussed above, mtime restoration is necessary because the Git working tree
will have all mtimes set to the checkout time. Clamping them to their
last-commit time (via git-restore-mtime from git-tools) makes the build
reproducible.
Publishing workflow
# .github/workflows/deploy.yaml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
paths: ["src/.well-known/webcat/**"]
jobs:
deploy:
uses: ./.github/workflows/build-and-deploy.yaml
secrets: inherit
Enrollment-sync workflow
# .github/workflows/sync-sigstore-enrollment.yaml
name: Sync Sigstore Enrollment
on:
schedule:
- cron: "0 3 * * *" # daily at 03:00 UTC
workflow_dispatch:
jobs:
sync:
permissions:
contents: write
pull-requests: write
uses: freedomofpress/webcat-cli/.github/workflows/sigstore-enrollment-sync.yml@main
with:
source-repository-uri: "https://github.com/${{ github.repository }}"
source-repository-ref: "${{ github.ref }}"
max_age: "15552000"
enrollment_path: src/.well-known/webcat/enrollment.json
enrollment_prev_path: src/.well-known/webcat/enrollment-prev.json
bundle_path: src/.well-known/webcat/bundle.json
bundle_prev_path: src/.well-known/webcat/bundle-prev.json