Skip to content

Commit f2ba137

Browse files
grubmeshiclaude
andcommitted
chore: set the repository up as a meshStack go-satellite
The CLI itself is not written yet, but the machinery that builds and tests it can be proven first. A satellite has to carry a descriptor at its root, publish its unit coverage where meshfed-release will look for it, and ask that repository to run the acceptance suite against a real backend, because a whole meshStack cannot run in a public repository's CI. This is the smallest repository that does all three. Nothing here is usable yet: the binary prints one line and exits, and the acceptance suite only asks a local dev stack for /mesh/info. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8f6cc2f commit f2ba137

17 files changed

Lines changed: 2075 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Ask the private meshStack mono repo to run this repository's acceptance suite and report the
2+
# result back as a check run. The suite needs a whole meshStack backend, so it cannot run here;
3+
# this workflow only asks for it.
4+
name: Acceptance Tests
5+
6+
# The push trigger is not decoration: running on the main push, and not only on pull requests, is
7+
# what surfaces a CLI/backend regression before a release tag.
8+
on:
9+
pull_request_target:
10+
push:
11+
branches:
12+
- main
13+
# TEMPORARY, delete before merge. A pull_request_target run takes this file from the base
14+
# branch, where it does not exist yet, so the dispatcher cannot try itself out on its own pull
15+
# request. A push event takes the pushed branch's own file, and is the only trigger that can.
16+
- feature/cli-satellite
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
# Reads "Acceptance Tests / request" in the checks list. No `name:`, unlike test.yml's jobs: a
23+
# name pins a stable string for a check that gates a merge, and nothing gates on this one. The
24+
# gating check is "Acceptance Tests (meshStack backend)", which meshfed-release posts.
25+
request:
26+
runs-on: ubuntu-latest
27+
env:
28+
SATELLITE_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.ref || github.ref_name }}
29+
30+
# Two things have to hold, and naming them once here keeps the steps below to one condition each.
31+
# The run has to be in our own repository, because a fork of it holds neither the app secrets nor
32+
# a branch anyone gates on. And on a pull request the head branch has to live in this repository,
33+
# which means its author has write access here: the code under test is then code we already trust.
34+
DISPATCH: ${{ github.repository_owner == 'meshcloud' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) }}
35+
36+
# This workflow MUST NOT check out the pull request, and has no `actions/checkout` for that
37+
# reason. `pull_request_target` runs in the base repo's context with its secrets, so checking out
38+
# contributor code here would be the classic "pwn request" hole. Reading `github.event` and
39+
# calling an API is passive use of that context and safe.
40+
steps:
41+
# Without this the contributor sees a required check that never reports and no reason for it.
42+
- name: Explain a skipped fork pull request
43+
if: env.DISPATCH != 'true' && github.event_name == 'pull_request_target'
44+
env:
45+
BASE_REPO: ${{ github.repository }}
46+
run: echo "::notice::Acceptance tests are not dispatched for a fork pull request. A maintainer has to adopt the branch into $BASE_REPO before the suite can run against it."
47+
48+
# Downscoped to `actions: write` at mint time even though the installation carries nothing
49+
# else, so a later widening of the app cannot leak into this workflow.
50+
- name: Mint a token for the dispatch
51+
id: token
52+
if: env.DISPATCH == 'true'
53+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
54+
with:
55+
# The secret holds the numeric app id, not a client id, and that is fine: the action reads
56+
# `client-id` or the deprecated `app-id` into one value, and GitHub takes either as the JWT
57+
# issuer. The name stays because it is an organization secret every satellite reads.
58+
client-id: ${{ secrets.SATELLITE_GH_APP_ID }}
59+
private-key: ${{ secrets.SATELLITE_GH_APP_PRIVATE_KEY }}
60+
owner: meshcloud
61+
repositories: meshfed-release
62+
permission-actions: write
63+
64+
# meshfed-release pairs a satellite branch with a same-named branch of its own, and the dispatch
65+
# names that branch rather than always `develop`. A `workflow_dispatch` reads both the workflow
66+
# file and the checkout it makes from the ref it is given, so dispatching to `develop` would run
67+
# the orchestration that is already merged and never the change to it that a paired branch
68+
# carries. This token may dispatch workflows in meshfed-release and read nothing there, so a
69+
# rejected dispatch is the only branch lookup available here. It also answers the better
70+
# question: not whether the branch exists, but whether it carries a workflow this can dispatch.
71+
#
72+
# `$SATELLITE_REF` reaches the script through the environment, and never as a `${{ }}` expression
73+
# that GitHub would substitute into the script text before bash reads it. A branch named `$(id)`
74+
# would otherwise run as a command.
75+
- name: Request the acceptance run
76+
if: env.DISPATCH == 'true'
77+
env:
78+
GH_TOKEN: ${{ steps.token.outputs.token }}
79+
run: |
80+
set -euo pipefail
81+
request() {
82+
gh workflow run ci-satellite.yml \
83+
--repo meshcloud/meshfed-release \
84+
--ref "$1" \
85+
-f repo=meshstack-cli \
86+
-f branch_name="$SATELLITE_REF"
87+
}
88+
if request "$SATELLITE_REF"; then
89+
orchestrated_from="$SATELLITE_REF"
90+
else
91+
echo "::notice::meshfed-release has no branch $SATELLITE_REF to dispatch, so develop orchestrates this run."
92+
request develop
93+
orchestrated_from=develop
94+
fi
95+
echo "::notice::Requested an acceptance run for $SATELLITE_REF, orchestrated from meshfed-release $orchestrated_from. The result arrives as the \"Acceptance Tests (meshStack backend)\" check."

.github/workflows/test.yml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# meshStack CLI build, lint and test workflow.
2+
name: Tests
3+
4+
# No `paths-ignore` on either trigger, deliberately: a workflow skipped that way never reports its
5+
# checks, so a required check on it stays "expected" forever and blocks the merge. A skipped *job*
6+
# reports success; a skipped *workflow* does not.
7+
on:
8+
pull_request:
9+
push:
10+
branches:
11+
- main
12+
13+
# Testing only needs permissions to read the repository contents.
14+
permissions:
15+
contents: read
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
build:
23+
name: Go Build
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
27+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
28+
with:
29+
go-version-file: 'go.mod'
30+
cache: true
31+
- run: go mod tidy
32+
- run: go build -v ./...
33+
- name: git diff
34+
run: |
35+
git diff --compact-summary --exit-code || \
36+
(echo; echo "Unexpected difference in directories after 'go mod tidy'. Run 'go mod tidy' command and commit."; exit 1)
37+
38+
golangci:
39+
needs: [ build ]
40+
name: Go Lint and Format Check
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
44+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
45+
with:
46+
# The repository's pinned Go, because it is what builds the linter in the next step.
47+
go-version-file: 'go.mod'
48+
cache: true
49+
# Built here rather than downloaded by the action below, because golangci-lint's formatters
50+
# use the go/format compiled into the binary: the formatting they enforce comes from the Go
51+
# release that BUILT the linter, not from the toolchain on PATH. The tool directive in go.mod
52+
# is the single pin, so no version is repeated here, and `go install` puts it where the action
53+
# will find it.
54+
- name: Build the pinned golangci-lint
55+
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint
56+
- name: golangci-lint
57+
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
58+
with:
59+
# The step above already installed it; the action is here for the annotations it puts on
60+
# the pull request diff, which a bare `run:` does not produce.
61+
install-mode: none
62+
# Deliberately no only-new-issues: this repository starts clean and CI keeps it that way,
63+
# so filtering to changed code cannot help and can only hide a finding.
64+
- name: Suggest fix command on failure
65+
if: failure()
66+
run: |
67+
echo "::error::Linting or formatting issues detected. Run 'task lint -- --fix' locally to automatically fix these issues, then commit the changes."
68+
69+
test:
70+
name: Go Test
71+
needs: [ build ]
72+
runs-on: ubuntu-latest
73+
steps:
74+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
75+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
76+
with:
77+
go-version-file: 'go.mod'
78+
cache: true
79+
# Coverage lands in a binary coverage-data directory (GOCOVERDIR format) rather than a text
80+
# profile, because only the binary form can be merged with the acceptance run's coverage — and
81+
# the instrumented binaries flush it on exit even when a test fails. `-coverpkg=./...` has to
82+
# match what meshfed-release runs the acceptance suite with: instrument different package sets
83+
# and the merged figure is a ratio over two different denominators.
84+
- name: Run unit tests with gotestsum
85+
run: |
86+
mkdir -p covdata/unit
87+
go tool gotestsum --junitfile junit.xml --format testdox -- \
88+
-coverpkg=./... ./... -args -test.gocoverdir="$PWD/covdata/unit"
89+
90+
# meshfed-release merges this with its acceptance coverage and posts the single coverage
91+
# comment. It finds the run by the pull request head sha, so this has to be uploaded from a
92+
# `pull_request` job. Contract with that side: the name `covdata-unit`, and covmeta.*/
93+
# covcounters.* at the artifact root — which naming the directory gives, where a glob would
94+
# nest them.
95+
- name: Upload unit coverage data
96+
if: always()
97+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
98+
with:
99+
name: covdata-unit
100+
path: covdata/unit
101+
retention-days: 3
102+
103+
# The merged comment needs an acceptance run; this figure does not, so it is still reported on
104+
# a fork pull request and on a push to main.
105+
- name: Report unit coverage
106+
if: always()
107+
run: |
108+
if ! ls covdata/unit/covmeta.* >/dev/null 2>&1; then
109+
echo "Unit coverage: no data produced." >> "$GITHUB_STEP_SUMMARY"
110+
exit 0
111+
fi
112+
go tool covdata textfmt -i=covdata/unit -o=unit.txt
113+
echo "Unit coverage: $(go tool cover -func=unit.txt | tail -1 | awk '{print $NF}')" >> "$GITHUB_STEP_SUMMARY"

.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Binary produced by 'task build'
2+
/meshstack
3+
4+
# Release artifacts produced by goreleaser
5+
/dist/
6+
7+
# Go environment created by the Nix dev shell (flake.nix shellHook)
8+
/.nix-go/
9+
10+
# Store symlink left behind by 'nix build'
11+
/result
12+
/result-*
13+
14+
# Local meshStack credentials, read by the Taskfile's dotenv
15+
.env
16+
17+
# A go.work names other checkouts by path, so it describes one developer's or one CI runner's
18+
# directory layout and never the repository. meshfed-release's go-satellite plugin writes one when
19+
# it runs the acceptance suite, and the meshStack Terraform provider writes one to build against a
20+
# paired branch of this repository.
21+
go.work
22+
go.work.sum
23+
24+
# What the CI test command leaves behind when you reproduce it locally.
25+
covdata/
26+
junit.xml
27+
unit.txt
28+
29+
# Editor and IDE directories
30+
.vscode/
31+
.idea/
32+
33+
# Per-developer Claude Code settings; .claude/settings.json is shared and committed
34+
.claude/settings.local.json

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
version: "2"

0 commit comments

Comments
 (0)