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: 4 additions & 0 deletions .github/workflows/ci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@ on:

jobs:
lint:
# Matches what metanorma/ci's ci-lint.yml declares for itself: yamllint,
# shellcheck and actionlint over a checkout, nothing more.
permissions:
contents: read
uses: metanorma/ci/.github/workflows/ci-lint.yml@main

9 changes: 9 additions & 0 deletions .github/workflows/ci-repo-watcher.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ on:

jobs:
watcher:
# `issues: write` is the load-bearing half. metanorma/ci's ci-repo-watcher.yml
# declares `contents: read` + `issues: write` for itself, and when it finds
# repos missing from cimas.yml it opens or updates an issue via
# metanorma/ci/comment-or-create. A caller granting less caps the called
# workflow, so trimming this to `contents: read` would fail that step on a
# daily cron nobody watches — the watcher would simply stop reporting.
permissions:
contents: read
issues: write
uses: metanorma/ci/.github/workflows/ci-repo-watcher.yml@main
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .github/workflows/ci-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ on:

jobs:
spec:
# Checkout and rspec; this job publishes nothing.
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
21 changes: 21 additions & 0 deletions cimas-config/gh-actions/data/check-index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,25 @@ jobs:
# branding lives in relaton/support's `data-index/configs.yml`, which
# data-deploy.yml reads at build time — this run resolves it too, so a broken
# configs.yml row fails the PR rather than the publish.
#
# Deliberately NARROWER than deploy.yml's grant, though both call the same
# reusable workflow. deploy.yml must grant the union of every scope
# data-deploy.yml's jobs request, including `pages: write` + `id-token: write`
# for its `deploy` job. Here that job cannot run at all: it is gated on
# `github.ref` equalling the repo's default branch, and a pull_request run's
# ref is `refs/pull/N/merge`. A job that is skipped is never dispatched, so
# its permission request is never evaluated — the union failure surfaces at
# the deploy step of a real build, not when the workflow graph is parsed.
#
# Which is why this is `contents: read` and must stay there: `pull_request` is
# the most reachable trigger in the repo, and the two scopes withheld are
# exactly the two that publish. Granting them here would hand every PR in
# every data repo the ability to deploy Pages, to buy nothing.
#
# If a synced repo's first PR ever fails with "requesting 'pages: write', but
# is only allowed 'pages: none'", that reading was wrong and the fix is to
# widen this to deploy.yml's three — not to touch the `if:` gate.
# Pinned by spec/workflow_permissions_spec.rb.
permissions:
contents: read
uses: relaton/support/.github/workflows/data-deploy.yml@main
4 changes: 4 additions & 0 deletions cimas-config/gh-actions/data/check_data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ on:

jobs:
check-data:
# check-data.yml checks out the repo (with submodules) and runs
# `bundle exec ruby check_data.rb`. Nothing writes, so read is the union.
permissions:
contents: read
uses: relaton/support/.github/workflows/check-data.yml@main
12 changes: 12 additions & 0 deletions cimas-config/gh-actions/data/crawler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ on:

jobs:
crawl:
# Required, not hardening. The shared crawler.yml's "Push data" step runs
# `git commit` + `git push` with the credentials actions/checkout persists,
# i.e. GITHUB_TOKEN — and a caller granting less caps what the called
# workflow gets. With only `contents: read` the crawl still runs to
# completion (166,658 documents on the largest flavor) and then fails on the
# push, having thrown the whole corpus away.
#
# The grant cannot live in crawler.yml itself: a called workflow's
# `permissions:` can only downgrade the caller's. Pinned by
# spec/data_deploy_caller_template_spec.rb.
permissions:
contents: write
uses: relaton/support/.github/workflows/crawler.yml@main
with:
args: ${{ github.event.inputs.args }}
19 changes: 19 additions & 0 deletions cimas-config/gh-actions/data/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,23 @@ jobs:
# defaulted to `gem`. That default is now `git`, so those pins set the value
# they would inherit anyway and a sync dropping them is the cleanup, not the
# damage. Do not re-add one here.
#
# `permissions:` is the one thing this job does declare, and it is
# deliberately wider than `contents: read`. A called workflow cannot exceed
# what the caller granted, so a caller needs the UNION of every scope every
# job of the called workflow asks for — data-deploy.yml declares
# `contents: read` at workflow level and adds `pages: write` +
# `id-token: write` on its own `deploy` job.
#
# Narrowing this to `contents: read` — the reflexive "minimal permissions"
# edit — does not fail at parse time. It fails minutes into the build, at the
# deploy step, in all 30 repos at once, with:
#
# The workflow is requesting 'pages: write', but is only allowed 'pages: none'.
#
# Pinned by spec/data_deploy_caller_template_spec.rb. Do not narrow it.
permissions:
contents: read # the four checkouts in build_index_page
pages: write # actions/deploy-pages in the deploy job
id-token: write # OIDC verification for that deployment
uses: relaton/support/.github/workflows/data-deploy.yml@main
31 changes: 30 additions & 1 deletion cimas-config/gh-actions/master/keep-alive.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
name: keep-alive

# This template had drifted away from what is actually deployed. Every data repo
# carried a hand-fixed copy — a `permissions:` block, a `workflow_dispatch:`
# trigger, and a `uses:` pointing at relaton/support — that the source template
# never picked up, so the next `cimas sync` would have reverted all three at once
# with nothing red in CI to say so. That is the failure mode of every hand-edit
# in a synced destination. The three differences are pinned individually by
# spec/workflow_permissions_spec.rb so they cannot drift apart again.
on:
schedule:
- cron: "0 0 1 * *" # on the first day-of-month
# A keep-alive whose only trigger is a monthly cron is exactly the workflow
# GitHub disables for inactivity — and once disabled there is no cron left to
# re-enable it. This is the manual way back in.
workflow_dispatch:

# Not an over-grant and not removable: re-enabling workflows through the Actions
# API (`gh api -X PUT /repos/.../actions/workflows/<id>/enable`) is this
# workflow's entire job. Dropping it reopens the CodeQL
# `actions/missing-workflow-permissions` alert that currently reads `fixed` in
# every data repo. Workflow-level rather than per-job, to match what is already
# deployed so a sync stays a functional no-op.
permissions:
actions: write

jobs:
keep-alive:
uses: metanorma/ci/.github/workflows/keep-alive.yml@main
# relaton/support's own copy, not metanorma/ci's. The fleet was deliberately
# moved onto it — every deployed keep-alive.yml points here — and only the
# template lagged. This is not a preference: metanorma/ci no longer has a
# keep-alive.yml at all, so the target this template used to name is gone and
# a sync would have pointed 40-odd repos at a 404. support's version also
# auto-discovers workflows by numeric id, which covers dynamic ones like
# CodeQL whose path is not under .github/workflows/ and so cannot be enabled
# by name.
uses: relaton/support/.github/workflows/keep-alive.yml@main
17 changes: 17 additions & 0 deletions cimas-config/gh-actions/master/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ on:

jobs:
release:
# The union of what relaton/support's release.yml declares for itself. A
# caller's grant is the ceiling for everything below it, so anything missing
# here fails at the publish step — after the version bump has already been
# committed and tagged, which is the worst possible place to find out.
#
# `packages: write` is here only because support's release.yml declares it.
# Nothing downstream asks for it: metanorma/ci's rubygems-release.yml grants
# its own jobs `contents: read` + `id-token: write` (preflight) and
# `contents: write` + `id-token: write` (release), and publishing goes to
# RubyGems, not the GitHub Packages registry. It is a real over-grant — but
# it cannot be dropped here alone, because removing it from the caller caps
# support's release.yml below what that file declares. Removing it means
# editing both, in one change, verified against a live release.
permissions:
id-token: write # OIDC trusted publishing to RubyGems
contents: write # the version bump commit and its tag
packages: write # see above — inherited over-grant, remove with release.yml
uses: relaton/support/.github/workflows/release.yml@main
with:
next_version: ${{ github.event.inputs.next_version }}
Expand Down
4 changes: 4 additions & 0 deletions cimas-config/gh-actions/model/make.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ on:

jobs:
make:
# make.yml checks out the repo and runs `bundle exec make clean all` across a
# three-OS matrix. It publishes nothing, so read is the union.
permissions:
contents: read
uses: relaton/support/.github/workflows/make.yml@main
42 changes: 42 additions & 0 deletions spec/data_deploy_caller_template_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,46 @@
expect(deploy_job).not_to have_key("with")
end
end

describe "the token grant it hands the shared workflow" do
# The counterpart to the block above: everything there is about what this
# caller must NOT carry, and `permissions:` is the one thing it must.
#
# The rule that makes this non-obvious: a called workflow cannot exceed what
# the caller granted, so the caller needs the UNION of every scope every job
# of the called workflow asks for — not the narrowest set that describes what
# the caller itself does. data-deploy.yml declares `contents: read` at
# workflow level and adds `pages: write` + `id-token: write` on its `deploy`
# job, so all three belong here.
#
# Narrowing this to `contents: read` — the reflexive "minimal permissions"
# edit — does not fail at parse time. It fails minutes into the build, at the
# deploy step, in all 30 repos at once, with:
#
# The workflow is requesting 'pages: write', but is only allowed 'pages: none'.
#
# Which is exactly why it is asserted here rather than left to review.
deploy_job = deploy.fetch("jobs").fetch("deploy")

it "grants the union of what data-deploy.yml's jobs request" do
expect(deploy_job.fetch("permissions"))
.to eq("contents" => "read", "pages" => "write", "id-token" => "write")
end

it "keeps the two Pages scopes specifically" do
# Stated separately from the exact-match example above because dropping
# either one is the actual failure mode, and a future edit that legitimately
# adds a fourth scope should still not be able to remove these two.
expect(deploy_job.fetch("permissions")).to include("pages" => "write", "id-token" => "write")
end

it "lets the crawler template push what it crawled" do
# `contents: write` is required, not hardening: the shared crawler.yml's
# "Push data" step runs `git commit` + `git push` with the credentials
# actions/checkout persists. Without it the crawl still runs to completion —
# 166,658 documents on the largest flavor — and then fails on the push.
expect(crawler.fetch("jobs").fetch("crawl").fetch("permissions"))
.to eq("contents" => "write")
end
end
end
Loading
Loading