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
107 changes: 107 additions & 0 deletions .github/workflows/provider-delete-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Provider Delete Guard

#
# Blocks any push that removes a provider from providers/src. Runs on pushes to
# every branch (feature and protected) and on pull requests to the protected
# branches so it can be used as a required status check.
#
# A provider is considered deleted if providers/src/<provider>/<version>/provider.yaml
# existed in the comparison base and no version of it exists in the pushed commit.
# Deleting or changing files within a provider is not affected.
#
# To intentionally delete a provider, add an explicit override naming it to a
# commit message in the push, e.g.
#
# [allow-provider-delete: netlify]
# [allow-provider-delete: netlify, deno]
#
# See docs/build-and-deployment.md and scripts/cicd/shell/provider-delete-guard.sh
#

on:
push:
branches:
- '**'
pull_request:
branches:
- main
- dev
workflow_dispatch:
inputs:
base:
description: "base ref to compare against (default: fork point with the default branch)"
required: false
default: ""

permissions:
contents: read

jobs:
provider-delete-guard:
name: provider-delete-guard
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
name: "[SETUP] checkout repo"
with:
fetch-depth: 0

- name: "[GUARD] resolve comparison base(s)"
id: bases
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
DISPATCH_BASE: ${{ github.event.inputs.base }}
run: |
set -euo pipefail
bases=()
null_sha="0000000000000000000000000000000000000000"

case "$EVENT_NAME" in
pull_request)
# HEAD is the PR merge commit; compare against the tip of the target branch
bases+=("$PR_BASE_SHA")
;;
push)
# 1. the previous tip of this branch (unavailable for new branches and
# for force pushes whose old tip is no longer reachable)
if [ "$BEFORE_SHA" != "$null_sha" ] && git rev-parse --verify --quiet "${BEFORE_SHA}^{commit}" >/dev/null; then
bases+=("$BEFORE_SHA")
else
echo "previous branch tip not available (new branch or force push)"
fi
# 2. the fork point with the default branch, so a new branch is checked
# against where it branched from (no-op when pushing to the default branch)
if merge_base="$(git merge-base HEAD "origin/${DEFAULT_BRANCH}" 2>/dev/null)"; then
bases+=("$merge_base")
fi
;;
workflow_dispatch)
if [ -n "$DISPATCH_BASE" ]; then
bases+=("$DISPATCH_BASE")
elif merge_base="$(git merge-base HEAD "origin/${DEFAULT_BRANCH}" 2>/dev/null)"; then
bases+=("$merge_base")
fi
;;
esac

# last resort: the first parent of the pushed commit
if [ "${#bases[@]}" -eq 0 ] && git rev-parse --verify --quiet 'HEAD~1^{commit}' >/dev/null; then
bases+=("HEAD~1")
fi

if [ "${#bases[@]}" -eq 0 ]; then
echo "::error::unable to resolve a base commit to compare against"
exit 1
fi

bases_str="$(printf '%s\n' "${bases[@]}" | sort -u | tr '\n' ' ')"
echo "comparison base(s): ${bases_str}"
echo "bases=${bases_str}" >> "$GITHUB_OUTPUT"

- name: "[GUARD] check for deleted providers"
run: |
bash scripts/cicd/shell/provider-delete-guard.sh HEAD ${{ steps.bases.outputs.bases }}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Once you have an OpenAPI specification, you can use the [openapisaurus](https://

The provider registry is built and deployed using GitHub Actions. Provider documents are validated and tested in workflow steps and then packaged and stored in the artifact repository. The most recent packaged versions are published to the registry API (a [Deno Deploy](https://deno.com/deploy) application), where they are available from the `stackql` application using `REGISTRY LIST` or `REGISTRY PULL`. See [docs/build-and-deployment.md](docs/build-and-deployment.md) for more information.

A separate workflow guards against providers being deleted from `providers/src` on any push; intentional removals require an explicit override in the commit message. See [provider delete guard](docs/build-and-deployment.md#provider-delete-guard) for details.

## Testing a Provider using the `dev` Registry

Use the following steps to test a provider using the `dev` registry:
Expand Down
38 changes: 38 additions & 0 deletions docs/build-and-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The nodes in the above graph are described in the sections below:
* [provider tests](#provider-tests)
* [register and store artifacts](#register-and-store-artifacts)
* [serve from Cloudflare](#serve-from-cloudflare)
* [provider delete guard](#provider-delete-guard)
<!--te-->

The following steps are performed on all pull requests to protected branches `dev` or `main` (if providers were updated):
Expand Down Expand Up @@ -94,3 +95,40 @@ The public StackQL Provider Registry is served from Cloudflare, using the follow
| --- | --- |
| [registry.stackql.app](https://registry.stackql.app/ping) | Production registry (built from `main`) |
| [registry-dev.stackql.app](https://registry-dev.stackql.app/ping) | Development registry (built from `dev`) |

## Provider Delete Guard

A separate workflow, [provider-delete-guard.yml](../.github/workflows/provider-delete-guard.yml), runs on every push to any branch (feature or protected) and on pull requests to `dev` or `main`. It fails the run if any provider under `providers/src` that existed in the comparison base no longer exists in the pushed commit. This is a safety net against a regeneration script or a bad merge silently removing a provider from the registry source.

A provider is considered present in a commit if `providers/src/<provider>/<version>/provider.yaml` exists for at least one version. Adding, changing or deleting files within a provider (for example regenerating service docs) does not trigger the guard.

The pushed commit is compared against:

| Event | Comparison base(s) |
| --- | --- |
| push to an existing branch | the previous tip of the branch, and the fork point with the default branch |
| push creating a new branch (or a force push whose old tip is unreachable) | the fork point with the default branch |
| pull request | the tip of the target branch |
| manual run (`workflow_dispatch`) | the `base` input, or the fork point with the default branch |

The check is implemented in [scripts/cicd/shell/provider-delete-guard.sh](../scripts/cicd/shell/provider-delete-guard.sh) and can be run locally against any two refs, for example:

```bash
bash scripts/cicd/shell/provider-delete-guard.sh HEAD origin/dev
```

### Overriding the guard

If a provider is being intentionally removed, add an explicit override token naming each provider to the message of a commit in the push (the commit that deletes the provider, or any later commit in the same push). Every deleted provider must be named; wildcards are not supported.

```text
[allow-provider-delete: netlify]
[allow-provider-delete: netlify, deno]
```

The guard reports allowed deletions as warnings in the job log and summary, so the override is visible in the run and permanently recorded in the git history. When merging a branch that deletes a provider using a squash merge, make sure the override token is retained in the squash commit message.

Notes:

- Removing a provider from `providers/src` does not remove previously published versions from the artifact repository or the registry; those are managed separately.
- GitHub skips all push and pull request workflows, including this guard, when the head commit message contains `[skip ci]`. Do not use `[skip ci]` on commits that touch `providers/src`.
Loading
Loading