tangier 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tangier-0.2.1/.github/actions/build/action.yaml +152 -0
- tangier-0.2.1/.github/actions/deploy/action.yaml +93 -0
- tangier-0.2.1/.github/actions/tailnet/action.yaml +119 -0
- tangier-0.2.1/.github/workflows/build.yaml +87 -0
- tangier-0.2.1/.github/workflows/ci.yaml +143 -0
- tangier-0.2.1/.github/workflows/release.yaml +35 -0
- tangier-0.2.1/.github/workflows/test-empty-matrix.yaml +38 -0
- tangier-0.2.1/.gitignore +10 -0
- tangier-0.2.1/CONTRIBUTING.md +34 -0
- tangier-0.2.1/PKG-INFO +121 -0
- tangier-0.2.1/README.md +114 -0
- tangier-0.2.1/bin/check-no-dependencies +36 -0
- tangier-0.2.1/bin/parity-check +250 -0
- tangier-0.2.1/bin/release +81 -0
- tangier-0.2.1/bin/test +5 -0
- tangier-0.2.1/docs/actions/tailnet.md +134 -0
- tangier-0.2.1/docs/guide/1-map-your-app.md +105 -0
- tangier-0.2.1/docs/guide/2-selective-tests.md +88 -0
- tangier-0.2.1/docs/guide/3-gates.md +135 -0
- tangier-0.2.1/docs/guide/4-packages-and-builds.md +102 -0
- tangier-0.2.1/docs/guide/5-github-actions.md +201 -0
- tangier-0.2.1/docs/guide/6-other-features.md +70 -0
- tangier-0.2.1/docs/parity-evidence.txt +487 -0
- tangier-0.2.1/docs/specs/changemap.md +191 -0
- tangier-0.2.1/docs/specs/gate.md +459 -0
- tangier-0.2.1/pipeline.example.toml +140 -0
- tangier-0.2.1/pipeline.toml +42 -0
- tangier-0.2.1/pyproject.toml +30 -0
- tangier-0.2.1/skills/tangier/SKILL.md +21 -0
- tangier-0.2.1/skills/tangier/ci.md +64 -0
- tangier-0.2.1/skills/tangier/gate.md +76 -0
- tangier-0.2.1/skills/tangier/reference/cli.md +189 -0
- tangier-0.2.1/skills/tangier/reference/github-actions.md +366 -0
- tangier-0.2.1/skills/tangier/reference/pipeline-toml.md +240 -0
- tangier-0.2.1/skills/tangier/setup.md +89 -0
- tangier-0.2.1/tangier/__init__.py +15 -0
- tangier-0.2.1/tangier/__main__.py +5 -0
- tangier-0.2.1/tangier/changemap.py +310 -0
- tangier-0.2.1/tangier/cli.py +164 -0
- tangier-0.2.1/tangier/commands/__init__.py +1 -0
- tangier-0.2.1/tangier/commands/args.py +18 -0
- tangier-0.2.1/tangier/commands/changemap_cmds.py +243 -0
- tangier-0.2.1/tangier/commands/deploy_cmds.py +323 -0
- tangier-0.2.1/tangier/commands/gate_cmds.py +822 -0
- tangier-0.2.1/tangier/commands/image_cmds.py +155 -0
- tangier-0.2.1/tangier/commands/tailnet_cmds.py +106 -0
- tangier-0.2.1/tangier/config.py +972 -0
- tangier-0.2.1/tangier/deploy.py +194 -0
- tangier-0.2.1/tangier/gate.py +745 -0
- tangier-0.2.1/tangier/git.py +246 -0
- tangier-0.2.1/tangier/github.py +43 -0
- tangier-0.2.1/tangier/globs.py +79 -0
- tangier-0.2.1/tangier/image.py +167 -0
- tangier-0.2.1/tangier/jobs.py +459 -0
- tangier-0.2.1/tangier/ranon.py +43 -0
- tangier-0.2.1/tangier/runner.py +219 -0
- tangier-0.2.1/tangier/tailnet.py +67 -0
- tangier-0.2.1/tangier/tests/__init__.py +16 -0
- tangier-0.2.1/tangier/tests/fixtures/compose.tmpl.yml +31 -0
- tangier-0.2.1/tangier/tests/fixtures/synthetic.toml +100 -0
- tangier-0.2.1/tangier/tests/support.py +201 -0
- tangier-0.2.1/tangier/tests/test_changemap.py +453 -0
- tangier-0.2.1/tangier/tests/test_changemap_cmds.py +461 -0
- tangier-0.2.1/tangier/tests/test_config.py +745 -0
- tangier-0.2.1/tangier/tests/test_deploy.py +694 -0
- tangier-0.2.1/tangier/tests/test_gate.py +1933 -0
- tangier-0.2.1/tangier/tests/test_gate_jobs.py +543 -0
- tangier-0.2.1/tangier/tests/test_github.py +51 -0
- tangier-0.2.1/tangier/tests/test_globs.py +64 -0
- tangier-0.2.1/tangier/tests/test_image.py +420 -0
- tangier-0.2.1/tangier/tests/test_image_golden.py +112 -0
- tangier-0.2.1/tangier/tests/test_tailnet.py +193 -0
- tangier-0.2.1/tangier/tests/test_version.py +14 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
name: tangier build
|
|
2
|
+
description: >-
|
|
3
|
+
Build and push one package's content-addressed image, skipping the build
|
|
4
|
+
entirely when that tag is already published.
|
|
5
|
+
|
|
6
|
+
inputs:
|
|
7
|
+
package:
|
|
8
|
+
description: The SHA bucket to build. Must have an `[image.<bucket>]` table in pipeline.toml.
|
|
9
|
+
required: true
|
|
10
|
+
registry-password:
|
|
11
|
+
description: Password for the image registry.
|
|
12
|
+
required: true
|
|
13
|
+
registry-username:
|
|
14
|
+
description: >-
|
|
15
|
+
Username for the image registry. Defaults to `registry`, which is what
|
|
16
|
+
every repo but tangerinecms uses.
|
|
17
|
+
required: false
|
|
18
|
+
default: registry
|
|
19
|
+
extra-secrets:
|
|
20
|
+
description: >-
|
|
21
|
+
Space-separated buildx secret IDS — not values — for secrets not named in
|
|
22
|
+
the package's `[image.<bucket>] secrets`. The value is read from the
|
|
23
|
+
environment variable of the same name upper-cased, so `sentry_auth_token`
|
|
24
|
+
reads SENTRY_AUTH_TOKEN. A secret whose variable is unset is silently
|
|
25
|
+
dropped rather than passed empty, so pass values through the step's `env:`
|
|
26
|
+
block (a composite action cannot read the `secrets` context).
|
|
27
|
+
required: false
|
|
28
|
+
default: ""
|
|
29
|
+
regctl-version:
|
|
30
|
+
description: regctl release to install. Pinned, never `latest`.
|
|
31
|
+
required: false
|
|
32
|
+
default: v0.6.1
|
|
33
|
+
tangier-ref:
|
|
34
|
+
description: >-
|
|
35
|
+
The tangier ref to install the CLI from. Defaults to the ref this action
|
|
36
|
+
was called at. Required when calling the action locally (`uses: ./...`),
|
|
37
|
+
because `github.action_ref` is empty for a local path.
|
|
38
|
+
required: false
|
|
39
|
+
default: ""
|
|
40
|
+
|
|
41
|
+
outputs:
|
|
42
|
+
tag:
|
|
43
|
+
description: The content-hash tag for this package, whether or not a build ran.
|
|
44
|
+
value: ${{ steps.build.outputs.tag }}
|
|
45
|
+
published:
|
|
46
|
+
description: >-
|
|
47
|
+
`true` when this run built and pushed the image, `false` when the tag was
|
|
48
|
+
already published and the build was skipped.
|
|
49
|
+
value: ${{ steps.build.outputs.built }}
|
|
50
|
+
|
|
51
|
+
runs:
|
|
52
|
+
using: composite
|
|
53
|
+
steps:
|
|
54
|
+
- name: Resolve the tangier ref
|
|
55
|
+
id: ref
|
|
56
|
+
shell: bash
|
|
57
|
+
# `github.action_ref` is empty inside a `run:` body but correct when bound
|
|
58
|
+
# through `env:` (actions/runner#2473), so every read here goes via the
|
|
59
|
+
# environment. It is also empty for a local `uses: ./...`, which is how
|
|
60
|
+
# tangier tests its own actions — hence the input, and a hard error rather
|
|
61
|
+
# than a silent fallback to `main`.
|
|
62
|
+
env:
|
|
63
|
+
ACTION_REF: ${{ github.action_ref }}
|
|
64
|
+
INPUT_REF: ${{ inputs.tangier-ref }}
|
|
65
|
+
run: |
|
|
66
|
+
set -euo pipefail
|
|
67
|
+
REF="${INPUT_REF:-$ACTION_REF}"
|
|
68
|
+
if [ -z "$REF" ]; then
|
|
69
|
+
echo "::error::cannot determine which tangier ref to install. Pass the tangier-ref input when calling this action by local path." >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
|
73
|
+
|
|
74
|
+
- uses: astral-sh/setup-uv@v5
|
|
75
|
+
with:
|
|
76
|
+
version: "0.5.29"
|
|
77
|
+
|
|
78
|
+
- name: Install tangier
|
|
79
|
+
shell: bash
|
|
80
|
+
# `uv tool install`, not `uvx --from`, so `tangier` stays on PATH for any
|
|
81
|
+
# repo script that runs after this action.
|
|
82
|
+
env:
|
|
83
|
+
TANGIER_REF: ${{ steps.ref.outputs.ref }}
|
|
84
|
+
run: |
|
|
85
|
+
set -euo pipefail
|
|
86
|
+
uv tool install "git+https://github.com/sminnee/tangier@${TANGIER_REF}"
|
|
87
|
+
|
|
88
|
+
- name: Install regctl
|
|
89
|
+
shell: bash
|
|
90
|
+
# Pinned by version, never `releases/latest`: a moving binary in the hot
|
|
91
|
+
# path of every build is a silent behaviour change with no PR anywhere.
|
|
92
|
+
# `set -euo pipefail` matters here — without it a failed curl leaves a
|
|
93
|
+
# zero-byte file that fails confusingly several steps later.
|
|
94
|
+
env:
|
|
95
|
+
REGCTL_VERSION: ${{ inputs.regctl-version }}
|
|
96
|
+
run: |
|
|
97
|
+
set -euo pipefail
|
|
98
|
+
# Into $RUNNER_TEMP rather than /usr/local/bin: that directory happens to
|
|
99
|
+
# be writable on GitHub-hosted images, but not on a self-hosted runner or
|
|
100
|
+
# a container job running as non-root — and `runs-on` is already an input.
|
|
101
|
+
mkdir -p "$RUNNER_TEMP/bin"
|
|
102
|
+
curl -fsSL -o "$RUNNER_TEMP/bin/regctl" \
|
|
103
|
+
"https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64"
|
|
104
|
+
chmod 755 "$RUNNER_TEMP/bin/regctl"
|
|
105
|
+
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
|
106
|
+
"$RUNNER_TEMP/bin/regctl" version
|
|
107
|
+
|
|
108
|
+
- uses: docker/setup-buildx-action@v3
|
|
109
|
+
|
|
110
|
+
- name: Registry login
|
|
111
|
+
shell: bash
|
|
112
|
+
# `[registry] url` is host + namespace, but `docker login` takes only the
|
|
113
|
+
# host: `${URL%%/*}` gives `registry.sminn.ee` from
|
|
114
|
+
# `registry.sminn.ee/tangerine`, and keeps a `:port` if one is configured.
|
|
115
|
+
# Read with tomllib rather than the CLI so this step does not depend on
|
|
116
|
+
# the install above having succeeded.
|
|
117
|
+
env:
|
|
118
|
+
REGISTRY_USERNAME: ${{ inputs.registry-username }}
|
|
119
|
+
REGISTRY_PASSWORD: ${{ inputs.registry-password }}
|
|
120
|
+
run: |
|
|
121
|
+
set -euo pipefail
|
|
122
|
+
URL="$(python3 -c '
|
|
123
|
+
import tomllib
|
|
124
|
+
with open("pipeline.toml", "rb") as fh:
|
|
125
|
+
print(tomllib.load(fh)["registry"]["url"])
|
|
126
|
+
')"
|
|
127
|
+
HOST="${URL%%/*}"
|
|
128
|
+
echo "Logging in to $HOST"
|
|
129
|
+
printf '%s' "$REGISTRY_PASSWORD" | docker login "$HOST" \
|
|
130
|
+
--username "$REGISTRY_USERNAME" --password-stdin
|
|
131
|
+
|
|
132
|
+
- name: Build and push
|
|
133
|
+
id: build
|
|
134
|
+
shell: bash
|
|
135
|
+
# tangier probes the registry first and skips the build when the
|
|
136
|
+
# content-hash tag is already published — the idempotence that
|
|
137
|
+
# content-addressed tagging exists for. It emits `tag` and `built` to
|
|
138
|
+
# $GITHUB_OUTPUT itself, which is where this action's outputs come from.
|
|
139
|
+
#
|
|
140
|
+
# `resolve_secrets` reads os.environ and drops any secret whose variable
|
|
141
|
+
# is unset, so a caller supplies values through this step's `env:` and
|
|
142
|
+
# needs no input for the ids already in `[image.<bucket>] secrets`.
|
|
143
|
+
env:
|
|
144
|
+
PACKAGE: ${{ inputs.package }}
|
|
145
|
+
EXTRA_SECRETS: ${{ inputs.extra-secrets }}
|
|
146
|
+
run: |
|
|
147
|
+
set -euo pipefail
|
|
148
|
+
ARGS=()
|
|
149
|
+
for id in $EXTRA_SECRETS; do
|
|
150
|
+
ARGS+=(--secret "id=$id")
|
|
151
|
+
done
|
|
152
|
+
tangier image build "$PACKAGE" --push "${ARGS[@]+"${ARGS[@]}"}"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
name: tangier deploy
|
|
2
|
+
description: >-
|
|
3
|
+
Connect to the tailnet, then render and apply this environment's manifests
|
|
4
|
+
with the current content-addressed image tags.
|
|
5
|
+
|
|
6
|
+
# READ docs/actions/tailnet.md BEFORE changing a call site. The separation
|
|
7
|
+
# between uat and prod is enforced by the `environment:` line on the calling
|
|
8
|
+
# job, and it is no longer visible from here.
|
|
9
|
+
|
|
10
|
+
inputs:
|
|
11
|
+
env:
|
|
12
|
+
description: Environment name. Must have a `[deploy.<env>]` table in pipeline.toml.
|
|
13
|
+
required: true
|
|
14
|
+
client-id:
|
|
15
|
+
description: >-
|
|
16
|
+
OAuth client ID for this environment's deploy identity. Pass
|
|
17
|
+
`${{ vars.TS_DEPLOY_CLIENT_ID }}` — a composite action cannot read the
|
|
18
|
+
`vars` context (actions/runner#2551).
|
|
19
|
+
required: true
|
|
20
|
+
audience:
|
|
21
|
+
description: OIDC audience for this environment. Pass `${{ vars.TS_DEPLOY_AUDIENCE }}`.
|
|
22
|
+
required: true
|
|
23
|
+
tailscale-version:
|
|
24
|
+
description: Tailscale release to install. Pinned, never `latest`.
|
|
25
|
+
required: false
|
|
26
|
+
default: "1.78.1"
|
|
27
|
+
tangier-ref:
|
|
28
|
+
description: >-
|
|
29
|
+
The tangier ref to install the CLI from. Defaults to the ref this action
|
|
30
|
+
was called at. Required when calling the action locally (`uses: ./...`),
|
|
31
|
+
because `github.action_ref` is empty for a local path.
|
|
32
|
+
required: false
|
|
33
|
+
default: ""
|
|
34
|
+
|
|
35
|
+
runs:
|
|
36
|
+
using: composite
|
|
37
|
+
steps:
|
|
38
|
+
- name: Resolve the tangier ref
|
|
39
|
+
id: ref
|
|
40
|
+
shell: bash
|
|
41
|
+
# Resolved at THIS action's top level and passed down, rather than left to
|
|
42
|
+
# the nested action: `github.action_ref` is only correct for the action it
|
|
43
|
+
# is evaluated in, and binding it through `env:` is required because it is
|
|
44
|
+
# empty inside a `run:` body (actions/runner#2473).
|
|
45
|
+
env:
|
|
46
|
+
ACTION_REF: ${{ github.action_ref }}
|
|
47
|
+
INPUT_REF: ${{ inputs.tangier-ref }}
|
|
48
|
+
run: |
|
|
49
|
+
set -euo pipefail
|
|
50
|
+
REF="${INPUT_REF:-$ACTION_REF}"
|
|
51
|
+
if [ -z "$REF" ]; then
|
|
52
|
+
echo "::error::cannot determine which tangier ref to install. Pass the tangier-ref input when calling this action by local path." >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
|
56
|
+
|
|
57
|
+
- uses: sminnee/tangier/.github/actions/tailnet@v0
|
|
58
|
+
with:
|
|
59
|
+
client-id: ${{ inputs.client-id }}
|
|
60
|
+
audience: ${{ inputs.audience }}
|
|
61
|
+
env: ${{ inputs.env }}
|
|
62
|
+
version: ${{ inputs.tailscale-version }}
|
|
63
|
+
|
|
64
|
+
- uses: astral-sh/setup-uv@v5
|
|
65
|
+
with:
|
|
66
|
+
version: "0.5.29"
|
|
67
|
+
|
|
68
|
+
- name: Install tangier
|
|
69
|
+
shell: bash
|
|
70
|
+
# `uv tool install`, not `uvx --from`: `tangier` must stay on PATH for any
|
|
71
|
+
# repo script that runs after this action.
|
|
72
|
+
env:
|
|
73
|
+
TANGIER_REF: ${{ steps.ref.outputs.ref }}
|
|
74
|
+
run: |
|
|
75
|
+
set -euo pipefail
|
|
76
|
+
uv tool install "git+https://github.com/sminnee/tangier@${TANGIER_REF}"
|
|
77
|
+
|
|
78
|
+
- name: Deploy
|
|
79
|
+
shell: bash
|
|
80
|
+
# `--summary` writes the build table to $GITHUB_STEP_SUMMARY before
|
|
81
|
+
# anything is applied — pass 1 overwrites the deployed tags it reads, and
|
|
82
|
+
# the table is most wanted when the deploy then fails.
|
|
83
|
+
#
|
|
84
|
+
# `set -euo pipefail` is a behaviour change from the bash this replaces,
|
|
85
|
+
# which had no `set -e` and fell through a failing summary into the real
|
|
86
|
+
# deploy. If it proves annoying, the relief valve is
|
|
87
|
+
# `continue-on-error: true` on a separate summary step — not dropping
|
|
88
|
+
# `set -e`.
|
|
89
|
+
env:
|
|
90
|
+
DEPLOY_ENV: ${{ inputs.env }}
|
|
91
|
+
run: |
|
|
92
|
+
set -euo pipefail
|
|
93
|
+
tangier deploy "$DEPLOY_ENV" --summary
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: tangier tailnet
|
|
2
|
+
description: >-
|
|
3
|
+
Connect the runner to the tailnet as this environment's deploy identity and
|
|
4
|
+
point kubectl at the Tailscale Kubernetes operator.
|
|
5
|
+
|
|
6
|
+
# READ docs/actions/tailnet.md BEFORE changing a call site. The separation
|
|
7
|
+
# between uat and prod is enforced by the `environment:` line on the calling
|
|
8
|
+
# job, and it is no longer visible from here.
|
|
9
|
+
|
|
10
|
+
inputs:
|
|
11
|
+
client-id:
|
|
12
|
+
description: >-
|
|
13
|
+
OAuth client ID for this environment's deploy identity. Pass
|
|
14
|
+
`${{ vars.TS_DEPLOY_CLIENT_ID }}` — a composite action cannot read the
|
|
15
|
+
`vars` context at all (actions/runner#2551), so it must be forwarded.
|
|
16
|
+
The value differs per environment; the NAME is deliberately the same.
|
|
17
|
+
required: true
|
|
18
|
+
audience:
|
|
19
|
+
description: >-
|
|
20
|
+
OIDC audience for this environment. Pass `${{ vars.TS_DEPLOY_AUDIENCE }}`.
|
|
21
|
+
Same reasoning, and the same deliberate name-sharing, as `client-id`.
|
|
22
|
+
required: true
|
|
23
|
+
env:
|
|
24
|
+
description: >-
|
|
25
|
+
Environment name, used to look up `[tailnet.<env>] tag` in pipeline.toml.
|
|
26
|
+
Omit when passing `tag` directly.
|
|
27
|
+
required: false
|
|
28
|
+
default: ""
|
|
29
|
+
tag:
|
|
30
|
+
description: >-
|
|
31
|
+
Tailnet ACL tag, overriding config. For a repo with no pipeline.toml —
|
|
32
|
+
k8s-cluster is helm and kubectl with no images, and a config describing
|
|
33
|
+
no tags, buckets or images would misrepresent it and trip
|
|
34
|
+
`_warn_tags_without_input`.
|
|
35
|
+
required: false
|
|
36
|
+
default: ""
|
|
37
|
+
operator:
|
|
38
|
+
description: >-
|
|
39
|
+
Tailscale Kubernetes operator hostname, overriding `[tailnet] operator`.
|
|
40
|
+
Defaults to `tailscale-operator` when there is no config to read.
|
|
41
|
+
required: false
|
|
42
|
+
default: ""
|
|
43
|
+
version:
|
|
44
|
+
description: Tailscale release to install. Pinned, never `latest`.
|
|
45
|
+
required: false
|
|
46
|
+
default: "1.78.1"
|
|
47
|
+
|
|
48
|
+
outputs:
|
|
49
|
+
tag:
|
|
50
|
+
description: The tailnet tag this runner authenticated as.
|
|
51
|
+
value: ${{ steps.resolve.outputs.tag }}
|
|
52
|
+
|
|
53
|
+
runs:
|
|
54
|
+
using: composite
|
|
55
|
+
steps:
|
|
56
|
+
- name: Resolve the tag and operator
|
|
57
|
+
id: resolve
|
|
58
|
+
shell: bash
|
|
59
|
+
# Read with inline tomllib rather than through the CLI: four of the eight
|
|
60
|
+
# call sites want the tailnet connect ALONE, and making them install uv
|
|
61
|
+
# and tangier to read one string would defeat the point of the action.
|
|
62
|
+
env:
|
|
63
|
+
INPUT_TAG: ${{ inputs.tag }}
|
|
64
|
+
INPUT_ENV: ${{ inputs.env }}
|
|
65
|
+
INPUT_OPERATOR: ${{ inputs.operator }}
|
|
66
|
+
run: |
|
|
67
|
+
set -euo pipefail
|
|
68
|
+
# Precedence: explicit input, then config. An explicit `tag` also means
|
|
69
|
+
# `pipeline.toml` need not exist at all — which is how a repo with no
|
|
70
|
+
# images (k8s-cluster is helm and kubectl) uses this action without a
|
|
71
|
+
# config file that would misrepresent it.
|
|
72
|
+
TAG="$INPUT_TAG"
|
|
73
|
+
OPERATOR="$INPUT_OPERATOR"
|
|
74
|
+
|
|
75
|
+
if [ -f pipeline.toml ]; then
|
|
76
|
+
# Two lines out, so an absent value stays distinguishable from an
|
|
77
|
+
# empty one without quoting games.
|
|
78
|
+
CFG="$(INPUT_ENV="$INPUT_ENV" python3 -c '
|
|
79
|
+
import os, tomllib
|
|
80
|
+
with open("pipeline.toml", "rb") as fh:
|
|
81
|
+
cfg = tomllib.load(fh).get("tailnet", {})
|
|
82
|
+
env = os.environ.get("INPUT_ENV", "")
|
|
83
|
+
print((cfg.get(env) or {}).get("tag", "") if env else "")
|
|
84
|
+
print(cfg.get("operator", "tailscale-operator"))
|
|
85
|
+
')"
|
|
86
|
+
CFG_TAG="$(printf '%s' "$CFG" | sed -n 1p)"
|
|
87
|
+
CFG_OPERATOR="$(printf '%s' "$CFG" | sed -n 2p)"
|
|
88
|
+
TAG="${TAG:-$CFG_TAG}"
|
|
89
|
+
OPERATOR="${OPERATOR:-$CFG_OPERATOR}"
|
|
90
|
+
fi
|
|
91
|
+
OPERATOR="${OPERATOR:-tailscale-operator}"
|
|
92
|
+
|
|
93
|
+
if [ -z "$TAG" ]; then
|
|
94
|
+
echo "::error::no tailnet tag for env [${INPUT_ENV:-<unset>}]. Add a [tailnet.<env>] table to pipeline.toml, or pass the tag input." >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
98
|
+
echo "operator=$OPERATOR" >> "$GITHUB_OUTPUT"
|
|
99
|
+
echo "Connecting as $TAG via $OPERATOR"
|
|
100
|
+
|
|
101
|
+
- name: Connect to the tailnet
|
|
102
|
+
uses: tailscale/github-action@v3
|
|
103
|
+
with:
|
|
104
|
+
oauth-client-id: ${{ inputs.client-id }}
|
|
105
|
+
tags: ${{ steps.resolve.outputs.tag }}
|
|
106
|
+
version: ${{ inputs.version }}
|
|
107
|
+
use-cache: "true"
|
|
108
|
+
args: --advertise-tags=${{ steps.resolve.outputs.tag }}
|
|
109
|
+
env:
|
|
110
|
+
TS_EXPERIMENT_OAUTH_AUDIENCE: ${{ inputs.audience }}
|
|
111
|
+
|
|
112
|
+
- name: Point kubectl at the operator
|
|
113
|
+
shell: bash
|
|
114
|
+
env:
|
|
115
|
+
OPERATOR: ${{ steps.resolve.outputs.operator }}
|
|
116
|
+
run: |
|
|
117
|
+
set -euo pipefail
|
|
118
|
+
tailscale configure kubeconfig "$OPERATOR"
|
|
119
|
+
kubectl config current-context
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: Build packages
|
|
2
|
+
|
|
3
|
+
# One matrixed job in place of a hand-written job per package. Feed `packages`
|
|
4
|
+
# from `tangier changemap build-matrix`, which emits exactly this shape.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
workflow_call:
|
|
8
|
+
inputs:
|
|
9
|
+
packages:
|
|
10
|
+
description: >-
|
|
11
|
+
JSON array of SHA buckets to build, e.g. '["astrochat","smartypants"]'.
|
|
12
|
+
A JSON *string* rather than a list because matrices need `fromJSON` and
|
|
13
|
+
Actions has no typed list input. `changemap build-matrix` emits it.
|
|
14
|
+
required: true
|
|
15
|
+
type: string
|
|
16
|
+
registry-username:
|
|
17
|
+
description: Registry username. Defaults to `registry`.
|
|
18
|
+
required: false
|
|
19
|
+
type: string
|
|
20
|
+
default: registry
|
|
21
|
+
extra-secrets:
|
|
22
|
+
description: Space-separated extra buildx secret IDs (not values).
|
|
23
|
+
required: false
|
|
24
|
+
type: string
|
|
25
|
+
default: ""
|
|
26
|
+
regctl-version:
|
|
27
|
+
required: false
|
|
28
|
+
type: string
|
|
29
|
+
default: v0.6.1
|
|
30
|
+
tangier-ref:
|
|
31
|
+
description: tangier ref to install the CLI from. Defaults to this workflow's ref.
|
|
32
|
+
required: false
|
|
33
|
+
type: string
|
|
34
|
+
default: ""
|
|
35
|
+
fail-fast:
|
|
36
|
+
description: >-
|
|
37
|
+
Cancel sibling legs when one fails. Defaults to false: an unrelated
|
|
38
|
+
package's failure should not discard a build that already succeeded,
|
|
39
|
+
and content-addressed tags make a partial set safe to resume.
|
|
40
|
+
required: false
|
|
41
|
+
type: boolean
|
|
42
|
+
default: false
|
|
43
|
+
runs-on:
|
|
44
|
+
required: false
|
|
45
|
+
type: string
|
|
46
|
+
default: ubuntu-latest
|
|
47
|
+
secrets:
|
|
48
|
+
registry-password:
|
|
49
|
+
required: true
|
|
50
|
+
sentry-auth-token:
|
|
51
|
+
description: >-
|
|
52
|
+
Passed to EVERY matrix leg — Actions offers no per-leg secret routing.
|
|
53
|
+
Safe because tangier fails closed: `resolve_secrets` emits `--secret`
|
|
54
|
+
only for ids the bucket's `[image.<bucket>] secrets` declares, so a
|
|
55
|
+
package that does not want it never references it.
|
|
56
|
+
required: false
|
|
57
|
+
|
|
58
|
+
jobs:
|
|
59
|
+
build:
|
|
60
|
+
# An empty `strategy.matrix` is a hard error ("Matrix vector 'package' does
|
|
61
|
+
# not contain any values"), not a skip — and any docs-only PR produces one.
|
|
62
|
+
# The quoting is load-bearing: `!= []` does not evaluate false for an empty
|
|
63
|
+
# array, so the comparison is against the JSON *string*.
|
|
64
|
+
if: ${{ inputs.packages != '[]' && inputs.packages != '' }}
|
|
65
|
+
runs-on: ${{ inputs.runs-on }}
|
|
66
|
+
strategy:
|
|
67
|
+
fail-fast: ${{ inputs.fail-fast }}
|
|
68
|
+
matrix:
|
|
69
|
+
package: ${{ fromJSON(inputs.packages) }}
|
|
70
|
+
name: build ${{ matrix.package }}
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
with:
|
|
74
|
+
# tangier hashes the git tree to derive the tag, so a shallow checkout
|
|
75
|
+
# would produce a different hash from a local run.
|
|
76
|
+
fetch-depth: 0
|
|
77
|
+
|
|
78
|
+
- uses: sminnee/tangier/.github/actions/build@v0
|
|
79
|
+
with:
|
|
80
|
+
package: ${{ matrix.package }}
|
|
81
|
+
registry-username: ${{ inputs.registry-username }}
|
|
82
|
+
registry-password: ${{ secrets.registry-password }}
|
|
83
|
+
extra-secrets: ${{ inputs.extra-secrets }}
|
|
84
|
+
regctl-version: ${{ inputs.regctl-version }}
|
|
85
|
+
tangier-ref: ${{ inputs.tangier-ref }}
|
|
86
|
+
env:
|
|
87
|
+
SENTRY_AUTH_TOKEN: ${{ secrets.sentry-auth-token }}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
# The Python versions are members of the `[gate.test]` group in pipeline.toml.
|
|
11
|
+
# One step for each member, so each version reports and reuses its own record.
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
# Depth 20 follows the checkout-depth rule in skills/tangier/reference/
|
|
15
|
+
# github-actions.md. These gates have no placeholder, so they need only HEAD^1.
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 20
|
|
19
|
+
- uses: astral-sh/setup-uv@v6
|
|
20
|
+
with:
|
|
21
|
+
# A verified gate never calls uv, and an empty cache fails the post step.
|
|
22
|
+
enable-cache: false
|
|
23
|
+
- name: Run test.py311
|
|
24
|
+
# No setup-python: the runner's own Python runs tangier, which is the
|
|
25
|
+
# environment tangier must work in.
|
|
26
|
+
env:
|
|
27
|
+
EVENT: ${{ github.event_name }}
|
|
28
|
+
run: |
|
|
29
|
+
# A pull request reuses a gate record. Any other event is the full
|
|
30
|
+
# build that does not consult records.
|
|
31
|
+
flags=(--read-only)
|
|
32
|
+
if [ "$EVENT" = pull_request ]; then flags+=(--base HEAD^1); else flags+=(--full); fi
|
|
33
|
+
python3 -m tangier gate run test.py311 "${flags[@]}"
|
|
34
|
+
- name: Run test.py312
|
|
35
|
+
env:
|
|
36
|
+
EVENT: ${{ github.event_name }}
|
|
37
|
+
run: |
|
|
38
|
+
flags=(--read-only)
|
|
39
|
+
if [ "$EVENT" = pull_request ]; then flags+=(--base HEAD^1); else flags+=(--full); fi
|
|
40
|
+
python3 -m tangier gate run test.py312 "${flags[@]}"
|
|
41
|
+
- name: Run test.py313
|
|
42
|
+
env:
|
|
43
|
+
EVENT: ${{ github.event_name }}
|
|
44
|
+
run: |
|
|
45
|
+
flags=(--read-only)
|
|
46
|
+
if [ "$EVENT" = pull_request ]; then flags+=(--base HEAD^1); else flags+=(--full); fi
|
|
47
|
+
python3 -m tangier gate run test.py313 "${flags[@]}"
|
|
48
|
+
- name: CLI smoke check
|
|
49
|
+
run: python3 -m tangier --help
|
|
50
|
+
|
|
51
|
+
no-dependencies:
|
|
52
|
+
# The check is `[gate.no-dependencies]` in pipeline.toml.
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v4
|
|
56
|
+
with:
|
|
57
|
+
fetch-depth: 20
|
|
58
|
+
- name: Run no-dependencies
|
|
59
|
+
# No setup-python: the runner's own Python is the one tangier must work on.
|
|
60
|
+
env:
|
|
61
|
+
EVENT: ${{ github.event_name }}
|
|
62
|
+
run: |
|
|
63
|
+
flags=(--read-only)
|
|
64
|
+
if [ "$EVENT" = pull_request ]; then flags+=(--base HEAD^1); else flags+=(--full); fi
|
|
65
|
+
python3 -m tangier gate run no-dependencies "${flags[@]}"
|
|
66
|
+
|
|
67
|
+
lint:
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
with:
|
|
72
|
+
fetch-depth: 20
|
|
73
|
+
- uses: astral-sh/setup-uv@v6
|
|
74
|
+
with:
|
|
75
|
+
enable-cache: false
|
|
76
|
+
- run: uv tool install ruff
|
|
77
|
+
- name: Run lint.check
|
|
78
|
+
env:
|
|
79
|
+
EVENT: ${{ github.event_name }}
|
|
80
|
+
run: |
|
|
81
|
+
flags=(--read-only)
|
|
82
|
+
if [ "$EVENT" = pull_request ]; then flags+=(--base HEAD^1); else flags+=(--full); fi
|
|
83
|
+
python3 -m tangier gate run lint.check "${flags[@]}"
|
|
84
|
+
- name: Run lint.format
|
|
85
|
+
env:
|
|
86
|
+
EVENT: ${{ github.event_name }}
|
|
87
|
+
run: |
|
|
88
|
+
flags=(--read-only)
|
|
89
|
+
if [ "$EVENT" = pull_request ]; then flags+=(--base HEAD^1); else flags+=(--full); fi
|
|
90
|
+
python3 -m tangier gate run lint.format "${flags[@]}"
|
|
91
|
+
|
|
92
|
+
lint-actions:
|
|
93
|
+
# `action-validator` covers `actions/*/action.yaml`, the file type nothing
|
|
94
|
+
# in this estate lints today. `actionlint` shellchecks every `run:` body,
|
|
95
|
+
# which is the higher-value of the two given these actions are mostly shell.
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
- name: action-validator
|
|
100
|
+
# One file per invocation — the CLI takes a single path.
|
|
101
|
+
run: |
|
|
102
|
+
set -euo pipefail
|
|
103
|
+
for f in .github/workflows/*.yaml .github/actions/*/action.yaml; do
|
|
104
|
+
echo "validating $f"
|
|
105
|
+
npx --yes @action-validator/cli@0.6.0 "$f"
|
|
106
|
+
done
|
|
107
|
+
- name: actionlint
|
|
108
|
+
# Covers workflows only — composite `action.yaml` files are the next step.
|
|
109
|
+
uses: docker://rhysd/actionlint:1.7.7
|
|
110
|
+
with:
|
|
111
|
+
args: -color
|
|
112
|
+
- name: shellcheck composite action run bodies
|
|
113
|
+
# actionlint shellchecks `run:` bodies in WORKFLOWS but does not read
|
|
114
|
+
# `actions/*/action.yaml`, so these scripts are otherwise unlinted — and
|
|
115
|
+
# they are mostly shell. Extract each body and check it directly.
|
|
116
|
+
run: |
|
|
117
|
+
set -euo pipefail
|
|
118
|
+
# Cleared, not just created: stale extracts from an earlier run would
|
|
119
|
+
# otherwise be checked against code no longer in the tree.
|
|
120
|
+
rm -rf "$RUNNER_TEMP/sc"
|
|
121
|
+
mkdir -p "$RUNNER_TEMP/sc"
|
|
122
|
+
python3 - <<'PY'
|
|
123
|
+
import os, re
|
|
124
|
+
out = os.path.join(os.environ["RUNNER_TEMP"], "sc")
|
|
125
|
+
for name in sorted(os.listdir(".github/actions")):
|
|
126
|
+
path = f".github/actions/{name}/action.yaml"
|
|
127
|
+
if not os.path.exists(path):
|
|
128
|
+
continue
|
|
129
|
+
text = open(path).read()
|
|
130
|
+
for i, block in enumerate(re.findall(r" run: \|\n((?: .*\n|\n)+)", text)):
|
|
131
|
+
body = "\n".join(line[8:] for line in block.splitlines())
|
|
132
|
+
with open(os.path.join(out, f"{name}_{i}.sh"), "w") as fh:
|
|
133
|
+
fh.write("#!/bin/bash\n" + body + "\n")
|
|
134
|
+
PY
|
|
135
|
+
count=$(find "$RUNNER_TEMP/sc" -name '*.sh' | wc -l)
|
|
136
|
+
test "$count" -gt 0 || { echo "::error::extracted no run bodies — the regex has drifted"; exit 1; }
|
|
137
|
+
echo "shellchecking $count run bodies, plus bin/"
|
|
138
|
+
find "$RUNNER_TEMP/sc" -name '*.sh' -print0 | xargs -0 shellcheck
|
|
139
|
+
# bin/ is otherwise ungated, and it holds the release script that
|
|
140
|
+
# force-moves the alias every consumer pins. `parity-check` is not in
|
|
141
|
+
# the list: it has a pre-existing SC2015 that predates this gate, and
|
|
142
|
+
# cleaning it belongs in its own change rather than here.
|
|
143
|
+
shellcheck bin/release bin/test
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publish to PyPI when an exact version tag is pushed. The `v0`
|
|
4
|
+
# alias does not match `v*.*.*`, so moving it publishes nothing.
|
|
5
|
+
#
|
|
6
|
+
# Trusted publishing, so the repo holds no PyPI token. CONTRIBUTING.md has the setup.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*.*.*"]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
id-token: write
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v6
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: false
|
|
24
|
+
- name: Check the tag matches the package version
|
|
25
|
+
env:
|
|
26
|
+
TAG: ${{ github.ref_name }}
|
|
27
|
+
run: |
|
|
28
|
+
version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
|
|
29
|
+
if [ "$TAG" != "v$version" ]; then
|
|
30
|
+
echo "::error::tag $TAG does not match pyproject.toml version $version"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
- run: uv build
|
|
34
|
+
# Pinned by commit: this job can publish as tangier, so a moved tag must not change what runs.
|
|
35
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Empty matrix guard
|
|
2
|
+
|
|
3
|
+
# Two lines guarding the footgun most likely to break a consumer's PR flow: a
|
|
4
|
+
# `strategy.matrix` over `[]` is a hard error in Actions, not a skip, and any
|
|
5
|
+
# docs-only PR produces exactly that. The assertion is that the run SUCCEEDS
|
|
6
|
+
# with the build job skipped.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
pull_request:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
empty:
|
|
15
|
+
uses: ./.github/workflows/build.yaml
|
|
16
|
+
with:
|
|
17
|
+
packages: "[]"
|
|
18
|
+
secrets:
|
|
19
|
+
registry-password: unused
|
|
20
|
+
|
|
21
|
+
assert-skipped:
|
|
22
|
+
needs: empty
|
|
23
|
+
if: ${{ always() }}
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- name: Assert the build job was skipped, not failed
|
|
27
|
+
env:
|
|
28
|
+
RESULT: ${{ needs.empty.result }}
|
|
29
|
+
run: |
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
# Exactly `skipped`. Accepting `success` too would let a regression
|
|
32
|
+
# that removed the `if:` guard pass whenever the matrix happened to
|
|
33
|
+
# run — which is the one thing this test exists to catch.
|
|
34
|
+
test "$RESULT" = skipped || {
|
|
35
|
+
echo "::error::expected the build job to be skipped, got result=$RESULT"
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
echo "empty matrix handled: build job skipped"
|