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
35 changes: 32 additions & 3 deletions .github/workflows/ci-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,34 @@ jobs:
# repository through the public pipeline.
meta=$(gh api "repos/$GITHUB_REPOSITORY")
private=$(printf '%s' "$meta" | jq -r '.private')
is_fork=$(printf '%s' "$meta" | jq -r '.fork')
topics=$(printf '%s' "$meta" | jq -r '.topics | join(" ")')
echo "private=$private" >> "$GITHUB_OUTPUT"
echo "Repository is private: $private"
echo "Topics: $topics"
echo "Repository is private: $private, fork: $is_fork"
echo "Topics: ${topics:-<none>}"

# A fork does not inherit its parent's topics. Verified against
# ktsu-dev/winget-pkgs, a fork of microsoft/winget-pkgs: the parent carries
# topics, the fork's repository object has none at all. Selecting a pipeline on
# a topic alone would therefore fail every fork of every ktsu repository, and
# fail it by telling a stranger to add organization metadata they do not own.
# Borrow the parent's topics instead, so a fork classifies as whatever it was
# forked from. The marker-file check below still applies, and files are
# inherited, so this loosens nothing about what a fork has to actually be.
if [ "$is_fork" = "true" ] \
&& [ "$(printf '%s' "$meta" | jq -r '.topics | index("dotnet") != null')" != "true" ]; then
parent=$(printf '%s' "$meta" | jq -r '.parent.full_name // empty')
if [ -n "$parent" ] && parent_meta=$(gh api "repos/$parent" 2>/dev/null); then
meta=$parent_meta
topics=$(printf '%s' "$meta" | jq -r '.topics | join(" ")')
echo "Fork of $parent; using its topics: ${topics:-<none>}"
else
# A parent that cannot be read -- deleted, or private to someone else --
# is not fatal on its own. Fall through and let the checks below report
# what is actually missing.
echo "Fork, but its parent could not be read; classifying on this repository alone."
fi
fi

# A topic says what the repository intends to be. A marker file says what it
# actually is. Requiring both is what stops the two ways this can go wrong:
Expand Down Expand Up @@ -94,7 +118,12 @@ jobs:

if [ "$has_global_json" = "true" ]; then
echo "::error::This repository has a global.json but not the 'dotnet' topic." >&2
echo "::error::Add the 'dotnet' topic; the topic is what selects the pipeline." >&2
if [ "$is_fork" = "true" ]; then
echo "::error::This is a fork and its parent's topics could not be read either." >&2
echo "::error::Add the 'dotnet' topic to this fork to build it." >&2
else
echo "::error::Add the 'dotnet' topic; the topic is what selects the pipeline." >&2
fi
exit 1
fi

Expand Down
24 changes: 24 additions & 0 deletions docs/shared-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ shape varies across `push`, `pull_request`, `schedule` and `workflow_dispatch`,
missing field would read as "public" — the one wrong answer that matters, because it
routes a private repository through the public pipeline.

### Forks

**A fork does not inherit its parent's topics.** Verified against `ktsu-dev/winget-pkgs`,
a fork of `microsoft/winget-pkgs`: the parent carries topics, the fork's repository object
has none at all. Selecting on a topic alone would therefore fail every fork of every ktsu
repository, and fail it by telling a stranger to add organization metadata they do not own.

So `detect` borrows the parent's topics when the repository is a fork and has no `dotnet`
topic of its own. A fork then classifies as whatever it was forked from. The marker-file
check still applies, and files *are* inherited, so this loosens nothing about what a fork
has to actually be. A fork that sets its own `dotnet` topic is taken at its word and the
parent is never consulted.

Nothing else needs a fork-specific gate, because the pipelines already have one each:

| Concern | Existing guard |
| --- | --- |
| Publishing | `ktsubuild` is passed `EXPECTED_OWNER: ktsu-dev` and decides `should_release` itself |
| SonarQube | every Sonar step is gated on `env.SONAR_TOKEN != ''`, which is empty in a fork |
| Installing the build tool | `dotnet tool install ktsu.KtsuBuild.Tool` from public NuGet, no secret |

A forker therefore gets discovery, build and tests across all three platforms, and the
steps that would need ktsu's secrets skip themselves.

## The caller

Each repository holds this at `.github/workflows/ci.yml`, byte-identical everywhere:
Expand Down