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/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,19 @@ pnpm test
```

Tests live next to the code they cover, as `*.test.ts`.

## Workflow self references

The workflows in this repository reference its own actions by commit SHA, exactly as a consuming repository does. Those pins do not move on their own, so after changing an action the workflows still run the previous copy of it until they are repointed.

`Check Self References` runs on every push to `main` and fails when a pin has fallen behind the action it points at. To resolve it:

```bash
script/bump-self-refs --fix
```

Commit the result and open a pull request. The check only compares a pin against the history of the action's own directory, so repointing the workflows does not itself make them stale again, even though merging the bump moves `main` to a new commit.

Keep a bump in a pull request of its own. `--fix` pins at the tip of the default branch, which is the only commit guaranteed to stay reachable once a branch is squashed or rebased, so a bump bundled with a change to an action pins to the commit before that change and the check fails again as soon as it merges. Change the action first, then bump.

The check runs on pushes to `main` rather than on pull requests, because while an action is being changed there is no commit yet to pin to. It reports after a merge, not before, so do not add `check_self_refs` to the repository's required status checks: it never reports on a pull request, and a pull request waiting on it would never be mergeable.
6 changes: 3 additions & 3 deletions .github/workflows/check-commit-messages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ jobs:
fetch-depth: 0

- name: Setup Python
uses: dfinity/ci-tools/actions/setup-python@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/setup-python@059c18686a7318d8b67fb361bc2d939b16b3959f # main

- name: Setup Commitizen
uses: dfinity/ci-tools/actions/setup-commitizen@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/setup-commitizen@059c18686a7318d8b67fb361bc2d939b16b3959f # main

- name: Check commit messages
uses: dfinity/ci-tools/actions/check-commit-messages@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/check-commit-messages@059c18686a7318d8b67fb361bc2d939b16b3959f # main
with:
target_branch: ${{ inputs.target_branch }}
6 changes: 3 additions & 3 deletions .github/workflows/check-pr-title.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: dfinity/ci-tools/actions/setup-python@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/setup-python@059c18686a7318d8b67fb361bc2d939b16b3959f # main

- name: Setup Commitizen
uses: dfinity/ci-tools/actions/setup-commitizen@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/setup-commitizen@059c18686a7318d8b67fb361bc2d939b16b3959f # main

- name: Check pull request title
uses: dfinity/ci-tools/actions/check-pr-title@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/check-pr-title@059c18686a7318d8b67fb361bc2d939b16b3959f # main
8 changes: 4 additions & 4 deletions .github/workflows/generate-changelog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,21 +135,21 @@ jobs:

- name: Setup Python
if: steps.release_commit.outputs.skip != 'true'
uses: dfinity/ci-tools/actions/setup-python@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/setup-python@059c18686a7318d8b67fb361bc2d939b16b3959f # main

- name: Setup Commitizen
if: steps.release_commit.outputs.skip != 'true'
uses: dfinity/ci-tools/actions/setup-commitizen@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/setup-commitizen@059c18686a7318d8b67fb361bc2d939b16b3959f # main

- name: Generate changelog
if: steps.release_commit.outputs.skip != 'true'
uses: dfinity/ci-tools/actions/generate-changelog@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/generate-changelog@059c18686a7318d8b67fb361bc2d939b16b3959f # main
with:
file_name: ${{ inputs.file_name }}

- name: Create pull request
if: steps.release_commit.outputs.skip != 'true'
uses: dfinity/ci-tools/actions/create-pr@afeee4fbdc0683a88ec5a74ed7f59a2ce0e833ad # main
uses: dfinity/ci-tools/actions/create-pr@059c18686a7318d8b67fb361bc2d939b16b3959f # main
with:
branch_name: ${{ inputs.branch_name }}
base_branch_name: ${{ inputs.base_branch_name }}
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/self-check-self-refs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Check Self References

on:
push:
branches:
- main
workflow_dispatch:

concurrency:
group: main-${{ github.workflow }}
cancel-in-progress: true

jobs:
# Deliberately not suffixed with `:required`. This job only runs after a merge,
# because while an action is being changed there is no commit yet to pin to, so
# it never reports on a pull request and cannot be a required status check.
check_self_refs:
name: check_self_refs
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# The check compares each pinned commit against the history of the
# action it points at, so the full history is needed.
fetch-depth: 0

- name: Check workflow self references
run: script/bump-self-refs
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
```

### Referencing your own actions

A reusable workflow that other repositories call must reference the actions in its own repository the same fully qualified, pinned way. A relative `./` path resolves against the caller's workspace rather than the repository the workflow lives in, so it fails there, and silently runs the caller's own file if one happens to sit at that path. Pinning keeps a single reference from a consuming repository resolving to a coherent set of workflows and actions.

The cost is that those pins do not move when an action changes, which leaves a workflow running an older copy of an action than the one beside it in the tree. That failure is quiet: a newly added input handed to an action pinned from before it existed is reported as a warning, not an error, so the run stays green while the new behaviour does nothing. Pair the convention with a job that fails when a pin has fallen behind:

```yaml
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Each pin is resolved against local history, which the default shallow
# checkout does not have.
fetch-depth: 0

- name: Check workflow self references
run: script/bump-self-refs
```

Run that job on pushes to the default branch rather than on pull requests. While an action is being changed there is no commit yet to pin to, so the pin can only be moved in a follow-up change, which also means the job cannot serve as a required status check.

The script in this repository resolves its own pins against its own history, so it checks this repository only. A repository that consumes these workflows has the same problem with its own pins, but solving it means comparing against a different repository's releases, which is what Dependabot's `github-actions` ecosystem is for.

## Managing Concurrency

For workflows that run on pull requests, use the `concurrency` key to ensure that only one workflow runs at a time for a given pull request. This prevents multiple workflows from running simultaneously and potentially causing conflicts.
Expand Down
139 changes: 139 additions & 0 deletions script/bump-self-refs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/bin/bash

# About:
#
# The workflows in this repository reference this repository's own actions by
# commit SHA, the same way other repositories reference them. That is what makes
# a single pinned reference resolve to a coherent set of workflows and actions,
# but those pins do not move when an action changes, so a workflow can keep
# running an older copy of an action than the one next to it in the tree.
#
# The failure is quiet. A workflow that passes a newly added input to an action
# pinned from before that input existed gets a warning rather than an error, so
# the run stays green while the new behaviour does nothing at all.
#
# This script reports every self reference whose action has changed since the
# commit it points at, and exits non-zero when it finds one.
#
# Usage:
#
# script/bump-self-refs report stale references
# script/bump-self-refs --fix repoint them when at least one has fallen behind

set -euo pipefail

REPO='dfinity/ci-tools'
WORKFLOW_DIR='.github/workflows'
REF_PATTERN="${REPO}/[A-Za-z0-9._/-]+@[0-9a-f]{40}"

fix=false
if [ "${1:-}" = '--fix' ]; then
fix=true
elif [ -n "${1:-}" ]; then
echo "Unknown argument '$1'. Usage: script/bump-self-refs [--fix]" >&2
exit 2
fi

cd "$(git rev-parse --show-toplevel)"

# Every pin is resolved against local history, which a shallow clone does not
# have. Without this the pins all report as unknown commits, which looks like a
# problem with the references rather than with the checkout.
if [ "$(git rev-parse --is-shallow-repository)" = 'true' ]; then
cat >&2 <<'EOF'
This repository is a shallow clone, so the commits the workflows pin cannot be
resolved. Fetch the full history first:

git fetch --unshallow locally
actions/checkout fetch-depth 0 in a workflow
EOF
exit 2
fi

# Built with a read loop rather than mapfile, which the bash 3.2 that ships with
# macOS does not have.
workflows=()
while IFS= read -r workflow; do
workflows+=("$workflow")
done < <(find "$WORKFLOW_DIR" -type f \( -name '*.yaml' -o -name '*.yml' \) | sort)

if [ -z "${workflows[0]:-}" ]; then
echo "No workflows found in $WORKFLOW_DIR" >&2
exit 2
fi

head_sha=$(git rev-parse HEAD)

refs=$(grep -hoE "$REF_PATTERN" "${workflows[@]}" | sort -u || true)
if [ -z "$refs" ]; then
echo "No $REPO self references found."
exit 0
fi

stale_count=0
while IFS= read -r ref; do
sha="${ref##*@}"
path="${ref%@*}"
path="${path#"$REPO"/}"

if [ "$sha" = "$head_sha" ]; then
continue
fi

# A pin to a commit that cannot be resolved is a problem in its own right, and
# there is nothing to compare it against.
if ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then
echo " ? ${path} pinned at ${sha:0:8}, which is not a known commit"
stale_count=$((stale_count + 1))
continue
fi

commits=$(git rev-list --count "${sha}..HEAD" -- "$path")
if [ "$commits" -gt 0 ]; then
echo " x ${path} pinned at ${sha:0:8}, ${commits} commit(s) behind"
stale_count=$((stale_count + 1))
fi
done <<EOF
$refs
EOF

if [ "$stale_count" -eq 0 ]; then
echo "All $REPO self references are up to date."
exit 0
fi

if [ "$fix" = false ]; then
cat >&2 <<EOF

${stale_count} self reference(s) point at an action that has changed since.
Run 'script/bump-self-refs --fix' and commit the result.
EOF
exit 1
fi

# A pin has to name a commit that stays reachable. Squash and rebase merges
# rewrite a branch's commits, so the default branch is the only safe target;
# HEAD would leave a reference to a commit that never lands.
default_ref=$(git symbolic-ref -q --short refs/remotes/origin/HEAD || echo 'origin/main')
target_sha=$(git rev-parse --verify -q "${default_ref}^{commit}" || true)

if [ -z "$target_sha" ]; then
echo "Cannot resolve ${default_ref}, so there is no safe commit to pin. Fetch it first." >&2
exit 2
fi

if [ "$target_sha" != "$head_sha" ]; then
echo
echo "Pinning at ${default_ref} (${target_sha:0:8}) rather than HEAD, which is not on it."
fi

# Once one pin has fallen behind, every self reference is repointed rather than
# only the stale ones, so that a single SHA describes the whole tree the way a
# consuming repository pins it.
REPO="$REPO" TARGET_SHA="$target_sha" perl -pi -e '
my $repo = quotemeta $ENV{REPO};
s/($repo\/[A-Za-z0-9._\/-]+\@)[0-9a-f]{40}/$1$ENV{TARGET_SHA}/g;
' "${workflows[@]}"

echo
echo "Repointed every $REPO self reference at ${target_sha:0:8}."
Loading