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
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,25 @@ updates:
labels:
- dependencies

# cascade pins every third-party action by commit SHA in one source of truth,
# internal/generate/action_pins.yaml. Dependabot cannot read that file, so
# .github/workflows/action-pins.yml is a non-executing anchor that lists every
# pinned action; Dependabot opens its github-actions bump PRs there (and in any
# hand-written workflow using the same action). A maintainer then copies the new
# pin into action_pins.yaml and regenerates the workflows. The action-pins.yml
# header documents the full flow.
#
# orchestrate.yaml and promote.yaml are generated by `cascade generate-workflow`
# from action_pins.yaml. Dependabot must not edit them: a bump there is
# overwritten on the next regeneration and fails the Workflow Drift Check.
# exclude-paths keeps Dependabot out of the generated files; their pins move
# only when the manifest is regenerated.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
labels:
- dependencies
exclude-paths:
- .github/workflows/orchestrate.yaml
- .github/workflows/promote.yaml
64 changes: 64 additions & 0 deletions .github/workflows/action-pins.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Dependabot anchor for cascade's pinned third-party actions.
#
# This workflow never runs. It exists only so Dependabot's github-actions
# updater has one well-known file that references every action cascade pins, and
# opens its version-bump pull requests against it. The real source of truth is
# internal/generate/action_pins.yaml, which Dependabot cannot read.
#
# How an action gets bumped now:
# 1. Dependabot bumps a `uses:` line here (and in any hand-written workflow
# that uses the same action).
# 2. CI turns red: the consistency lint (internal/generate,
# TestWorkflowsConsistentWithActionPins) and TestActionPinsAnchorCoversManifest
# report that the anchor and action_pins.yaml disagree.
# 3. A maintainer copies the new sha and version into action_pins.yaml, then
# runs `cascade generate-workflow --config .github/manifest.yaml --force` to
# refresh orchestrate.yaml and promote.yaml from the updated manifest.
# 4. CI turns green: the pin now agrees across the manifest, this anchor, the
# hand-written workflows, and the generated workflows.
#
# The generated workflows (orchestrate.yaml, promote.yaml) are excluded from
# Dependabot in .github/dependabot.yml, so a bump never lands there directly and
# trips the Workflow Drift Check. They move only through regeneration in step 3.
#
# Keep exactly one step per action key in action_pins.yaml.
# TestActionPinsAnchorCoversManifest fails if this list drifts out of sync.
name: Action Pins (dependabot anchor)

on:
workflow_dispatch:
inputs:
run_anchor:
description: 'Leave disabled. This workflow is a Dependabot parse anchor and performs no work.'
type: boolean
default: false

permissions:
contents: read

jobs:
anchor:
name: Pinned action references (never executes)
# Gated off by a default-false dispatch input so the workflow runs no steps
# under normal operation. Dependabot parses `uses:` statically and ignores
# both the trigger set and this condition, so the bump entrypoint still works
# while the job stays inert.
if: ${{ inputs.run_anchor }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: return
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
path: .
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: '0'
private-key: ''
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
- uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
55 changes: 55 additions & 0 deletions internal/generate/action_pins_anchor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package generate

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

// anchorWorkflowPath is the dependabot anchor workflow, relative to the repo's
// .github directory. It gives the github-actions updater one well-known file
// that references every action pinned in action_pins.yaml.
const anchorWorkflowPath = "workflows/action-pins.yml"

// TestActionPinsAnchorCoversManifest keeps the dependabot anchor a complete and
// correct mirror of action_pins.yaml.
//
// Completeness matters because Dependabot only bumps actions it can see in a
// scanned file. The generated workflows (orchestrate.yaml, promote.yaml) are
// excluded from Dependabot in .github/dependabot.yml, so an action that appears
// only there (for example actions/create-github-app-token) would never get a
// bump PR without a tracked home in the anchor.
//
// Correctness (each anchor ref matches the manifest sha and version) is also
// enforced repo-wide by TestWorkflowsConsistentWithActionPins; asserting it here
// too keeps the anchor's purpose self-contained, so a stale anchor is reported
// against the manifest directly rather than only as one row in the repo sweep.
func TestActionPinsAnchorCoversManifest(t *testing.T) {
manifest := loadActionPinsManifest(t)
githubDir := repoGitHubDir(t)

content, err := os.ReadFile(filepath.Join(githubDir, anchorWorkflowPath)) //nolint:gosec // fixed path under the repo's .github tree.
require.NoError(t, err)

// Correctness: no governed uses: line in the anchor may diverge from the manifest.
mismatches := scanUsesForPinDrift(anchorWorkflowPath, string(content), manifest)
require.Emptyf(t, mismatches, "dependabot anchor refs diverge from action_pins.yaml: %v", mismatches)

// Completeness: every manifest action must appear in the anchor so Dependabot
// has a tracked bump entrypoint for it.
anchored := make(map[string]bool)
for _, line := range strings.Split(string(content), "\n") {
if groups := usesRefRe.FindStringSubmatch(line); groups != nil {
anchored[groups[1]] = true
}
}
for action := range manifest {
require.Truef(t, anchored[action],
"action %q is in action_pins.yaml but missing from the dependabot anchor %s; "+
"add a `uses: %s@<sha> # <version>` step so Dependabot can bump it",
action, anchorWorkflowPath, action)
}
}
Loading