-
Notifications
You must be signed in to change notification settings - Fork 0
332 lines (320 loc) · 16.1 KB
/
Copy pathowen-cli-release.yml
File metadata and controls
332 lines (320 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
name: Owen.Cli release
# Release pipeline for the `owen` dotnet tool (public package Owen.Cli,
# internal project OwnSharp.Cli — public facade rebrand, PR #246; alpha gate
# A / issue #202). Separate from ci.yml's `ownsharp-cli-smoke` job on
# purpose: that job proves the packaging shape works on every push/PR; this
# workflow is specifically the RELEASE process — build, test, pack, prove
# the packed artifact installs clean on both OSes, then publish ONLY behind
# an explicit gate. See docs/notes/owen-cli-release.md for the versioning
# policy and the release checklist this workflow implements.
#
# Publish safety (two independent gates, both required):
# 1. `publish` only runs when the trigger is a pushed tag matching
# `owen-cli-v*` — a `workflow_dispatch` run (no such tag ref) can
# build/test/pack/smoke-test but can never reach the publish job. A
# `pull_request` run (below) has neither a tag ref nor `push` as its
# event_name, so the same condition also skips it there.
# 2. `publish` targets the `nuget-release` GitHub Environment, which a repo
# admin must configure with required reviewers (Settings -> Environments)
# before this can ever run unattended — see the release checklist. The
# job additionally self-checks that the environment actually has a
# required_reviewers rule (scripts/check_environment_protection.sh)
# before doing anything else, since GitHub auto-creates a referenced-
# but-never-configured environment with zero protection rules.
# The workflow never echoes secrets.NUGET_API_KEY; `dotnet nuget push` takes
# it as a CLI argument (not printed by the tool) and Actions' own log
# redaction masks any accidental echo of a registered secret value.
permissions:
contents: read
on:
push:
tags:
- "owen-cli-v*"
# Review: the green PR check on this workflow's own PRs never actually ran
# build-test-pack -> smoke-test (ubuntu + windows) -- only the tag-push
# trigger did, and no tag is ever pushed from a PR. `publish` stays fully
# skipped on a pull_request event (gate 1 above), so this only exercises
# the pack/inspect/install/smoke path, never the publish path.
pull_request:
paths:
- "frontend/roslyn/OwnSharp.Cli/**"
- "frontend/roslyn/OwnSharp.Extractor/**"
- "ownlang/**"
- ".github/workflows/owen-cli-release.yml"
- "scripts/check_environment_protection.sh"
workflow_dispatch: {}
env:
CLI_PROJECT: frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
jobs:
build-test-pack:
name: build + test + pack
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
# This job runs the whole Python suite below, which includes
# tests/test_checkpoint_status.py: it verifies that a recorded
# mutation campaign names a commit that exists and is an ancestor of
# HEAD, and a depth-1 checkout cannot answer that. ci.yml's tests job
# takes the history for the same reason.
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
- name: Standard repo gates (run_tests.py, ruff, mypy)
run: |
python tests/run_tests.py
pip install --quiet ruff mypy
ruff check .
mypy
- name: dotnet build (extractor + CLI)
run: dotnet build "$CLI_PROJECT" -c Release
# Versioning policy (docs/notes/owen-cli-release.md): the csproj
# <Version> is the single source of truth. On a tag push, the tag's
# version suffix must match it byte-for-byte, or this fails loudly
# instead of silently publishing the wrong version.
- name: Read the csproj Version
id: version
run: |
v=$(grep -oP '(?<=<Version>)[^<]+' "$CLI_PROJECT")
[ -n "$v" ] || { echo "FAIL: could not read <Version> from $CLI_PROJECT"; exit 1; }
echo "csproj Version: $v"
echo "version=$v" >> "$GITHUB_OUTPUT"
- name: On a tag push, assert the tag matches the csproj Version
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/owen-cli-v')
# env: indirection (CodeRabbit review), not `csproj_version="${{ ... }}"`
# inlined into the script -- this workflow also triggers on pull_request
# for paths that include the csproj this value is grep'd from, so a
# version string containing shell metacharacters must never be
# re-parsed as script text by bash.
env:
CSPROJ_VERSION: ${{ steps.version.outputs.version }}
run: |
tag_version="${GITHUB_REF_NAME#owen-cli-v}"
csproj_version="$CSPROJ_VERSION"
if [ "$tag_version" != "$csproj_version" ]; then
echo "FAIL: tag owen-cli-v$tag_version does not match csproj Version $csproj_version"
echo "Bump <Version> in $CLI_PROJECT to match the tag (or retag) before releasing."
exit 1
fi
echo "OK: tag matches csproj Version ($csproj_version)"
- name: dotnet pack
run: dotnet pack "$CLI_PROJECT" -c Release -o "$RUNNER_TEMP/nupkg"
- name: Inspect package contents (bundled runtime/core assets present)
run: |
set -euo pipefail
nupkg=$(ls "$RUNNER_TEMP"/nupkg/Owen.Cli.*.nupkg)
echo "package: $nupkg"
mkdir -p "$RUNNER_TEMP/nupkg-inspect"
unzip -q "$nupkg" -d "$RUNNER_TEMP/nupkg-inspect"
find "$RUNNER_TEMP/nupkg-inspect" -name "*.nuspec" -exec cat {} \;
# "ownsharp.dll"/"ownsharp-extract.dll" are the real, unrenamed
# internal filenames (AssemblyName, public facade rebrand PR #246)
# -- what actually ships, not a stale pre-rebrand reference.
test -f "$RUNNER_TEMP/nupkg-inspect/tools/net8.0/any/ownsharp.dll" \
|| { echo "FAIL: ownsharp.dll (the CLI itself) missing from the package"; exit 1; }
test -f "$RUNNER_TEMP/nupkg-inspect/tools/net8.0/any/ownsharp-extract.dll" \
|| { echo "FAIL: bundled extractor (ownsharp-extract.dll) missing from the package"; exit 1; }
core_py_count=$(find "$RUNNER_TEMP/nupkg-inspect/tools/net8.0/any/ownlang-core/ownlang" -name "*.py" 2>/dev/null | wc -l)
[ "$core_py_count" -gt 0 ] \
|| { echo "FAIL: vendored ownlang core .py files missing from the package"; exit 1; }
echo "OK: package contains the CLI, the bundled extractor, and $core_py_count vendored core .py files"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: owen-cli-nupkg
path: ${{ runner.temp }}/nupkg/*.nupkg
retention-days: 14
smoke-test:
name: install from the packed artifact — clean install -> check -> findings
needs: build-test-pack
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
# env: indirection (CodeRabbit review), not `${{ needs.build-test-pack.outputs.version }}`
# inlined directly into each run: script -- a job-level env var reads the
# same way everywhere below without bash ever re-parsing the expression
# as script text.
env:
OWEN_CLI_VERSION: ${{ needs.build-test-pack.outputs.version }}
steps:
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: owen-cli-nupkg
path: ${{ runner.temp }}/nupkg
- name: Put the dotnet global-tools shim dir on PATH
run: echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
# Critical test rule (docs/notes/owen-cli-release.md): this MUST
# install and run the packed .nupkg from build-test-pack, never a
# project reference or `dotnet run` — that would only prove the source
# exists, not that the package works for an end user. No checkout of
# this repo happens on this job at all: it only has the artifact.
#
# --add-source is NOT enough on its own (Codex review, PR #244):
# `dotnet tool install` queries every configured source (the machine's
# default nuget.org feed included) IN PARALLEL and takes whichever
# answers first — once a same-numbered version is ever actually on
# nuget.org (e.g. a rerun after a real publish), this install could
# silently resolve from there instead of the local artifact, defeating
# the whole point of this job. An isolated nuget.config with `<clear/>`
# removes the ambiguity: the ONLY source this install can see is the
# local artifact directory.
- name: Isolated NuGet.config — the packed artifact is the ONLY visible source
run: |
cat > "$RUNNER_TEMP/isolated-nuget.config" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local-artifact" value="$RUNNER_TEMP/nupkg" />
</packageSources>
</configuration>
EOF
- name: dotnet tool install --global from the packed artifact (isolated source)
run: dotnet tool install --global Owen.Cli --version "$OWEN_CLI_VERSION" --configfile "$RUNNER_TEMP/isolated-nuget.config"
- name: A minimal leak, in a scratch dir with no Own.NET checkout anywhere
run: |
mkdir -p "$RUNNER_TEMP/sample"
cat > "$RUNNER_TEMP/sample/Leak.cs" <<'EOF'
using System.IO;
public class Leaky
{
public void Run()
{
var s = new MemoryStream();
s.WriteByte(1);
}
}
EOF
- name: owen --version reports the released version
run: |
out=$(owen --version)
[ "$out" = "$OWEN_CLI_VERSION" ] \
|| { echo "FAIL: owen --version printed '$out', expected '$OWEN_CLI_VERSION'"; exit 1; }
- name: owen check finds the leak (--fail-on-finding exits 1, OWN001 present)
run: |
set +e
out=$(owen check "$RUNNER_TEMP/sample" --fail-on-finding 2>&1)
rc=$?
set -e
echo "$out"
[ "$rc" -eq 1 ] || { echo "FAIL: expected exit 1, got $rc"; exit 1; }
echo "$out" | grep -q "OWN001" || { echo "FAIL: expected OWN001 in the output"; exit 1; }
- name: owen check on clean code exits 0
run: |
mkdir -p "$RUNNER_TEMP/clean"
cat > "$RUNNER_TEMP/clean/Clean.cs" <<'EOF'
using System.IO;
public class Tidy
{
public void Run()
{
using var s = new MemoryStream();
s.WriteByte(1);
}
}
EOF
set +e
out=$(owen check "$RUNNER_TEMP/clean" --fail-on-finding 2>&1)
rc=$?
set -e
echo "$out"
[ "$rc" -eq 0 ] || { echo "FAIL: expected exit 0 on clean code, got $rc"; exit 1; }
- name: No Python found -> fast actionable failure (never an auto-download)
run: |
set +e
out=$(OWEN_PYTHON=/definitely/does/not/exist/python3 owen check "$RUNNER_TEMP/sample" 2>&1)
rc=$?
set -e
echo "$out"
[ "$rc" -eq 3 ] || { echo "FAIL: expected exit 3 (Python not found), got $rc"; exit 1; }
echo "$out" | grep -qi "OWEN_PYTHON" || { echo "FAIL: expected the OWEN_PYTHON-specific message"; exit 1; }
- name: Reinstall/update behavior — uninstall, then reinstall clean
run: |
dotnet tool uninstall --global Owen.Cli
if command -v owen >/dev/null 2>&1; then
echo "FAIL: owen still on PATH after uninstall"; exit 1
fi
dotnet tool install --global Owen.Cli --version "$OWEN_CLI_VERSION" --configfile "$RUNNER_TEMP/isolated-nuget.config"
out=$(owen --version)
[ "$out" = "$OWEN_CLI_VERSION" ] \
|| { echo "FAIL: reinstalled owen --version printed '$out'"; exit 1; }
echo "OK: uninstall -> reinstall reproduces a working install"
publish:
name: publish to nuget.org (protected)
needs: [build-test-pack, smoke-test]
# Both conditions are required (Codex review, PR #244): `github.ref` alone
# is not proof of a tag PUSH — `gh workflow run --ref owen-cli-v0.1.0`
# (a workflow_dispatch) sets github.ref to that same tag ref, which would
# satisfy a bare startsWith() check and let a manual dispatch reach
# publish after smoke/environment approval, contradicting the safety
# gate this workflow documents ("a workflow_dispatch run can never reach
# publish"). Requiring event_name == 'push' closes that hole.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/owen-cli-v')
runs-on: ubuntu-latest
environment: nuget-release
permissions:
contents: read
# Required to call GET /repos/.../environments/{name} below (Codex
# review: "the environment-read API requires Actions read permission
# for fine-grained repository tokens") -- contents:read alone is not
# enough for that specific endpoint.
actions: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
sparse-checkout: |
scripts
# GitHub auto-creates a REFERENCED-BUT-NEVER-CONFIGURED environment on
# first use, with NO protection rules (Codex review: "referencing a
# missing environment creates it and the newly created environment has
# 'no protection rules or secrets configured'"). `environment:
# nuget-release` above is therefore not itself proof a human ever
# approves this job -- the job-dispatch gate GitHub evaluates BEFORE
# any step runs already let this run through if the environment was
# never actually configured with required reviewers. This step is the
# loud, fail-closed check for that: it runs first, before the artifact
# is even downloaded, and refuses to publish unless the environment
# has a REQUIRED_REVIEWERS rule with at least one reviewer (Codex
# review: a bare protection_rules count also accepts a wait_timer- or
# branch_policy-only environment, neither of which waits for a
# human) -- scripts/check_environment_protection.sh is the single
# source of truth for that predicate, fixture-tested in ci.yml.
- name: Refuse to publish unless nuget-release actually has protection rules
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/environments/nuget-release" > "$RUNNER_TEMP/nuget-release-env.json"
./scripts/check_environment_protection.sh "$RUNNER_TEMP/nuget-release-env.json" \
|| { echo "::error::the 'nuget-release' GitHub Environment does not have a required_reviewers rule with at least one reviewer -- a repo admin must set that up under Settings -> Environments before a tag push can safely reach 'dotnet nuget push'. Refusing to publish."; exit 1; }
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: owen-cli-nupkg
path: ${{ runner.temp }}/nupkg
- name: dotnet nuget push (secret never echoed; Actions also masks it in logs)
run: |
nupkg=$(ls "$RUNNER_TEMP"/nupkg/Owen.Cli.*.nupkg)
dotnet nuget push "$nupkg" \
--api-key "${{ secrets.NUGET_API_KEY }}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate