src-auth-perms-sync 0.2.1__tar.gz → 0.3.0__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.
Files changed (78) hide show
  1. src_auth_perms_sync-0.3.0/.github/CODEOWNERS +1 -0
  2. src_auth_perms_sync-0.3.0/.github/workflows/ci.yml +17 -0
  3. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/.github/workflows/release.yml +178 -61
  4. src_auth_perms_sync-0.3.0/.github/workflows/validate.yml +267 -0
  5. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/.gitignore +3 -0
  6. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/AGENTS.md +31 -80
  7. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/PKG-INFO +89 -49
  8. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/README.md +87 -47
  9. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/dev/TODO.md +44 -8
  10. src_auth_perms_sync-0.3.0/dev/mapping-efficiency.md +175 -0
  11. src_auth_perms_sync-0.3.0/dev/memory-efficiency-analyze.py +618 -0
  12. src_auth_perms_sync-0.3.0/dev/memory-efficiency-generate.py +1142 -0
  13. src_auth_perms_sync-0.3.0/dev/memory-efficiency-monitor-sourcegraph.sh +348 -0
  14. src_auth_perms_sync-0.3.0/dev/memory-efficiency.md +341 -0
  15. src_auth_perms_sync-0.3.0/dev/test-end-to-end.py +3042 -0
  16. src_auth_perms_sync-0.3.0/maps-example.yaml +130 -0
  17. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/pyproject.toml +8 -5
  18. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/renovate.json +12 -0
  19. src_auth_perms_sync-0.3.0/src/src_auth_perms_sync/__init__.py +11 -0
  20. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/cli.py +340 -201
  21. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/orgs/sync.py +11 -11
  22. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/apply.py +58 -11
  23. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/command.py +121 -56
  24. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/full_set.py +52 -24
  25. src_auth_perms_sync-0.3.0/src/src_auth_perms_sync/permissions/mapping.py +836 -0
  26. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/maps.py +22 -26
  27. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/queries.py +31 -6
  28. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/restore.py +11 -3
  29. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/snapshot.py +26 -26
  30. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/sourcegraph.py +32 -19
  31. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/types.py +20 -12
  32. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/workflow.py +8 -26
  33. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/backups.py +0 -7
  34. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/queries.py +25 -12
  35. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/site_config.py +1 -1
  36. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/sourcegraph.py +9 -3
  37. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/types.py +7 -0
  38. src_auth_perms_sync-0.3.0/tests/integration/test_cli_entrypoint.py +76 -0
  39. src_auth_perms_sync-0.3.0/tests/unit/test_apply.py +102 -0
  40. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/tests/unit/test_backups.py +2 -14
  41. src_auth_perms_sync-0.3.0/tests/unit/test_cli_config.py +533 -0
  42. src_auth_perms_sync-0.3.0/tests/unit/test_maps.py +540 -0
  43. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/tests/unit/test_restore.py +5 -4
  44. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/tests/unit/test_snapshot.py +24 -33
  45. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/uv.lock +257 -30
  46. src_auth_perms_sync-0.2.1/.github/workflows/ci.yml +0 -77
  47. src_auth_perms_sync-0.2.1/dev/git-worktrees.md +0 -89
  48. src_auth_perms_sync-0.2.1/dev/test-command-permutations.py +0 -1817
  49. src_auth_perms_sync-0.2.1/dev/test-plan.md +0 -207
  50. src_auth_perms_sync-0.2.1/maps-example.yaml +0 -39
  51. src_auth_perms_sync-0.2.1/src/src_auth_perms_sync/__init__.py +0 -1
  52. src_auth_perms_sync-0.2.1/src/src_auth_perms_sync/permissions/mapping.py +0 -627
  53. src_auth_perms_sync-0.2.1/src/src_auth_perms_sync/shared/id_codec.py +0 -67
  54. src_auth_perms_sync-0.2.1/tests/integration/test_cli_entrypoint.py +0 -20
  55. src_auth_perms_sync-0.2.1/tests/unit/test_cli_config.py +0 -295
  56. src_auth_perms_sync-0.2.1/tests/unit/test_id_codec.py +0 -26
  57. src_auth_perms_sync-0.2.1/tests/unit/test_maps.py +0 -75
  58. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/.env.example +0 -0
  59. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/.markdownlint-cli2.yaml +0 -0
  60. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/.python-version +0 -0
  61. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/LICENSE +0 -0
  62. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/SECURITY.md +0 -0
  63. /src_auth_perms_sync-0.2.1/dev/dead-code-audit.md → /src_auth_perms_sync-0.3.0/dev/audit-dead-code.md +0 -0
  64. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/dev/hooks/pre-commit +0 -0
  65. /src_auth_perms_sync-0.2.1/dev/python-versions.md → /src_auth_perms_sync-0.3.0/dev/update-python-versions.md +0 -0
  66. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/__main__.py +0 -0
  67. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/orgs/__init__.py +0 -0
  68. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/orgs/command.py +0 -0
  69. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/orgs/queries.py +0 -0
  70. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/orgs/types.py +0 -0
  71. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/permissions/__init__.py +0 -0
  72. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/__init__.py +0 -0
  73. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/run_context.py +0 -0
  74. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/src/src_auth_perms_sync/shared/saml_groups.py +0 -0
  75. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/tests/__init__.py +0 -0
  76. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/tests/integration/__init__.py +0 -0
  77. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/tests/unit/__init__.py +0 -0
  78. {src_auth_perms_sync-0.2.1 → src_auth_perms_sync-0.3.0}/tests/unit/test_saml_groups.py +0 -0
@@ -0,0 +1 @@
1
+ * @marcleblanc2
@@ -0,0 +1,17 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ permissions:
7
+ contents: read
8
+ pull-requests: read
9
+
10
+ concurrency:
11
+ group: ci-${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ validate:
16
+ name: Validate
17
+ uses: ./.github/workflows/validate.yml
@@ -1,4 +1,4 @@
1
- name: Build customer release
1
+ name: Build release
2
2
 
3
3
  on:
4
4
  push:
@@ -6,16 +6,17 @@ on:
6
6
  - "v*"
7
7
  workflow_dispatch:
8
8
  inputs:
9
- tag:
10
- description: "Existing release tag to publish, for example v0.1.0"
9
+ version:
10
+ description: "Package version to publish, for example 0.1.0 or v0.1.0"
11
11
  required: true
12
12
  type: string
13
13
 
14
14
  permissions:
15
- contents: write
15
+ contents: read
16
+ pull-requests: read
16
17
 
17
18
  concurrency:
18
- group: release-${{ github.event.inputs.tag || github.ref_name }}
19
+ group: release-${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
19
20
  cancel-in-progress: false
20
21
 
21
22
  defaults:
@@ -23,13 +24,44 @@ defaults:
23
24
  shell: bash
24
25
 
25
26
  jobs:
27
+ release_ref:
28
+ name: Resolve release tag
29
+ runs-on: ubuntu-24.04
30
+ outputs:
31
+ tag: ${{ steps.release.outputs.tag }}
32
+ version: ${{ steps.release.outputs.version }}
33
+
34
+ steps:
35
+ - name: Resolve release tag
36
+ id: release
37
+ env:
38
+ RELEASE_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
39
+ run: |
40
+ release_input="${RELEASE_INPUT}"
41
+ if [[ ! "${release_input}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
42
+ echo "::error title=Invalid release version::Use MAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH, got '${release_input}'."
43
+ exit 1
44
+ fi
45
+
46
+ release_version="${release_input#v}"
47
+ release_tag="v${release_version}"
48
+ echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
49
+ echo "version=${release_version}" >> "${GITHUB_OUTPUT}"
50
+
51
+ validate:
52
+ name: Validate
53
+ needs: release_ref
54
+ uses: ./.github/workflows/validate.yml
55
+ with:
56
+ ref: ${{ needs.release_ref.outputs.tag }}
57
+ build-package: false
58
+
26
59
  wheelhouse:
27
60
  name: ${{ matrix.platform }}-py311 wheelhouse
61
+ needs: release_ref
28
62
  runs-on: ${{ matrix.runs_on }}
29
63
  strategy:
30
64
  fail-fast: false
31
- # The first matrix leg creates the release; later legs upload more assets.
32
- max-parallel: 1
33
65
  matrix:
34
66
  include:
35
67
  - platform: linux-x86_64
@@ -55,23 +87,30 @@ jobs:
55
87
  with:
56
88
  fetch-depth: 0
57
89
  persist-credentials: false
58
- ref: ${{ github.event.inputs.tag || github.ref }}
90
+ ref: ${{ needs.release_ref.outputs.tag }}
59
91
 
60
92
  - name: Set up Python
61
93
  uses: actions/setup-python@v6
62
94
  with:
63
95
  python-version: ${{ env.PYTHON_VERSION }}
64
- cache: pip
96
+
97
+ - name: Cache uv
98
+ uses: actions/cache@v5
99
+ with:
100
+ path: ~/.cache/uv
101
+ key: uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock') }}
102
+ restore-keys: |
103
+ uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-
65
104
 
66
105
  - name: Install build tools
67
- run: |
68
- python -m pip install --upgrade pip
69
- python -m pip install "uv==${UV_VERSION}"
106
+ run: python -m pip install "uv==${UV_VERSION}"
70
107
 
71
108
  - name: Validate release inputs
72
109
  id: release
110
+ env:
111
+ RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
73
112
  run: |
74
- release_tag="${{ github.event.inputs.tag || github.ref_name }}"
113
+ release_tag="${RELEASE_TAG}"
75
114
  if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
76
115
  echo "::error title=Invalid release tag::Use a vMAJOR.MINOR.PATCH tag, got '${release_tag}'."
77
116
  exit 1
@@ -89,21 +128,10 @@ jobs:
89
128
  exit 1
90
129
  fi
91
130
 
92
- project_version=$(uv run --frozen python - <<'PY'
93
- import tomllib
94
-
95
- with open("pyproject.toml", "rb") as pyproject_file:
96
- print(tomllib.load(pyproject_file)["project"]["version"])
97
- PY
98
- )
99
- if [[ "v${project_version}" != "${release_tag}" ]]; then
100
- echo "::error title=Version mismatch::pyproject.toml version '${project_version}' does not match tag '${release_tag}'."
101
- exit 1
102
- fi
103
-
104
131
  echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
132
+ echo "version=${release_tag#v}" >> "${GITHUB_OUTPUT}"
105
133
 
106
- - name: Validate package
134
+ - name: Validate runner architecture
107
135
  run: |
108
136
  actual_machine=$(uv run --frozen python - <<'PY'
109
137
  import platform
@@ -116,16 +144,11 @@ jobs:
116
144
  exit 1
117
145
  fi
118
146
 
119
- uv lock --check
120
- uv run --frozen ruff check src/src_auth_perms_sync/
121
- uv run --frozen ruff format --check src/src_auth_perms_sync/
122
- uv run --frozen pyright
123
- uv run --frozen src-auth-perms-sync --help >/tmp/src-auth-perms-sync-help.txt
124
-
125
147
  - name: Build wheelhouse tarball
126
148
  id: build
127
149
  run: |
128
150
  release_tag="${{ steps.release.outputs.tag }}"
151
+ release_version="${{ steps.release.outputs.version }}"
129
152
  release_dir="build/release/${ASSET_BASENAME}"
130
153
  wheelhouse_dir="${release_dir}/wheelhouse"
131
154
  dist_dir="build/release/dist"
@@ -136,32 +159,67 @@ jobs:
136
159
 
137
160
  rm -rf build/release
138
161
  mkdir -p "${wheelhouse_dir}" "${dist_dir}"
162
+ shopt -s nullglob
139
163
 
140
- uv build --wheel --out-dir "${dist_dir}" --no-create-gitignore
164
+ uv build --wheel --sdist --out-dir "${dist_dir}" --no-create-gitignore
141
165
  project_wheels=("${dist_dir}"/*.whl)
142
166
  if [[ "${#project_wheels[@]}" -ne 1 ]]; then
143
167
  echo "::error title=Unexpected wheel count::Expected one project wheel, found ${#project_wheels[@]}."
144
168
  exit 1
145
169
  fi
170
+ project_source_distributions=("${dist_dir}"/*.tar.gz)
171
+ if [[ "${#project_source_distributions[@]}" -ne 1 ]]; then
172
+ echo "::error title=Unexpected source distribution count::Expected one project source distribution, found ${#project_source_distributions[@]}."
173
+ exit 1
174
+ fi
146
175
  project_wheel_path="${project_wheels[0]}"
147
176
  project_wheel_name="$(basename "${project_wheel_path}")"
177
+ project_source_distribution_path="${project_source_distributions[0]}"
178
+ project_source_distribution_name="$(basename "${project_source_distribution_path}")"
179
+ case "${project_wheel_name}" in
180
+ src_auth_perms_sync-"${release_version}"-*.whl)
181
+ ;;
182
+ *)
183
+ echo "::error title=Wheel version mismatch::Expected wheel version ${release_version}, got '${project_wheel_name}'."
184
+ exit 1
185
+ ;;
186
+ esac
187
+ case "${project_source_distribution_name}" in
188
+ src_auth_perms_sync-"${release_version}".tar.gz)
189
+ ;;
190
+ *)
191
+ echo "::error title=Source distribution version mismatch::Expected source distribution version ${release_version}, got '${project_source_distribution_name}'."
192
+ exit 1
193
+ ;;
194
+ esac
195
+ project_wheel_checksum_path="${project_wheel_path}.sha256"
196
+ project_source_distribution_checksum_path="${project_source_distribution_path}.sha256"
148
197
  if [[ ! -f "${project_wheel_path}" ]]; then
149
198
  echo "::error title=Missing project wheel::Expected ${project_wheel_path} to exist."
150
199
  exit 1
151
200
  fi
152
201
 
153
- uv export \
154
- --no-dev \
155
- --no-emit-project \
156
- --no-hashes \
157
- --no-header \
158
- --no-annotate \
159
- --frozen \
160
- --output-file "${requirements_file}"
202
+ dependency_metadata_dir="$(mktemp -d)"
203
+ git clone --no-hardlinks . "${dependency_metadata_dir}" >/dev/null
204
+ (
205
+ cd "${dependency_metadata_dir}"
206
+ git checkout --detach "${release_tag}" >/dev/null
207
+ mkdir -p "$(dirname "${requirements_file}")"
208
+ uv export \
209
+ --no-sources \
210
+ --no-dev \
211
+ --no-emit-project \
212
+ --no-hashes \
213
+ --no-header \
214
+ --no-annotate \
215
+ --output-file "${requirements_file}"
216
+ )
161
217
 
218
+ cp "${dependency_metadata_dir}/${requirements_file}" "${requirements_file}"
162
219
  cp "${requirements_file}" "${runtime_requirements_file}"
163
- if grep -q '^\./' "${runtime_requirements_file}"; then
220
+ if grep -Eq '(^-e[[:space:]]|^(\.\.?/)|(^|[[:space:]])file:| @ (\.\.?/|file:))' "${runtime_requirements_file}"; then
164
221
  echo "::error title=Unexpected local dependency::Runtime requirements must resolve from PyPI."
222
+ cat "${runtime_requirements_file}"
165
223
  exit 1
166
224
  fi
167
225
 
@@ -197,7 +255,7 @@ jobs:
197
255
  pip install "https://github.com/sourcegraph/src-auth-perms-sync/releases/download/${release_tag}/${project_wheel_name}"
198
256
  EOF
199
257
 
200
- (cd "${wheelhouse_dir}" && shasum -a 256 *.whl > WHEELS.sha256)
258
+ (cd "${wheelhouse_dir}" && shasum -a 256 ./*.whl > WHEELS.sha256)
201
259
 
202
260
  test -f "${project_wheel_path}"
203
261
  test -f "${wheelhouse_dir}"/src_auth_perms_sync-*.whl
@@ -211,16 +269,28 @@ jobs:
211
269
  exit 1
212
270
  fi
213
271
 
272
+ (
273
+ cd "$(dirname "${project_wheel_path}")"
274
+ shasum -a 256 "${project_wheel_name}" > "$(basename "${project_wheel_checksum_path}")"
275
+ shasum -a 256 "${project_source_distribution_name}" > "$(basename "${project_source_distribution_checksum_path}")"
276
+ )
277
+
214
278
  tar -C "${release_dir}" -czf "${asset_path}" wheelhouse
215
279
  (
216
280
  cd "$(dirname "${asset_path}")"
217
281
  shasum -a 256 "$(basename "${asset_path}")" > "$(basename "${checksum_path}")"
218
282
  )
219
283
 
220
- echo "asset_path=${asset_path}" >> "${GITHUB_OUTPUT}"
221
- echo "checksum_path=${checksum_path}" >> "${GITHUB_OUTPUT}"
222
- echo "project_wheel_path=${project_wheel_path}" >> "${GITHUB_OUTPUT}"
223
- echo "project_wheel_name=${project_wheel_name}" >> "${GITHUB_OUTPUT}"
284
+ {
285
+ echo "asset_path=${asset_path}"
286
+ echo "checksum_path=${checksum_path}"
287
+ echo "project_wheel_path=${project_wheel_path}"
288
+ echo "project_wheel_name=${project_wheel_name}"
289
+ echo "project_source_distribution_path=${project_source_distribution_path}"
290
+ echo "project_source_distribution_name=${project_source_distribution_name}"
291
+ echo "project_wheel_checksum_path=${project_wheel_checksum_path}"
292
+ echo "project_source_distribution_checksum_path=${project_source_distribution_checksum_path}"
293
+ } >> "${GITHUB_OUTPUT}"
224
294
 
225
295
  - name: Validate offline install from tarball
226
296
  run: |
@@ -244,6 +314,7 @@ jobs:
244
314
  run: |
245
315
  release_tag="${{ steps.release.outputs.tag }}"
246
316
  project_wheel_name="${{ steps.build.outputs.project_wheel_name }}"
317
+ project_source_distribution_name="${{ steps.build.outputs.project_source_distribution_name }}"
247
318
  notes_path="build/release/release-notes.md"
248
319
  cat > "${notes_path}" <<EOF
249
320
  ## Customer install
@@ -273,7 +344,9 @@ jobs:
273
344
  \`\`\`
274
345
 
275
346
  The tarball includes this project, \`src-py-lib\`, and all runtime wheels.
276
- Verify the download with the matching \`.sha256\` file.
347
+ Verify the tarball downloads with the matching \`.sha256\` files.
348
+ The GitHub release also includes the same \`${project_wheel_name}\` and
349
+ \`${project_source_distribution_name}\` files uploaded to PyPI, plus matching checksums.
277
350
 
278
351
  ### Connected PyPI install
279
352
 
@@ -296,29 +369,72 @@ jobs:
296
369
  path: |
297
370
  ${{ steps.build.outputs.asset_path }}
298
371
  ${{ steps.build.outputs.checksum_path }}
372
+
373
+ - name: Upload project distribution release artifact
374
+ if: matrix.platform == 'linux-x86_64'
375
+ uses: actions/upload-artifact@v7
376
+ with:
377
+ name: src-auth-perms-sync-project-distributions
378
+ path: |
299
379
  ${{ steps.build.outputs.project_wheel_path }}
300
- ${{ steps.notes.outputs.path }}
380
+ ${{ steps.build.outputs.project_source_distribution_path }}
381
+ ${{ steps.build.outputs.project_wheel_checksum_path }}
382
+ ${{ steps.build.outputs.project_source_distribution_checksum_path }}
383
+
384
+ - name: Upload release notes artifact
385
+ if: matrix.platform == 'linux-x86_64'
386
+ uses: actions/upload-artifact@v7
387
+ with:
388
+ name: release-notes
389
+ path: ${{ steps.notes.outputs.path }}
301
390
 
302
391
  - name: Upload PyPI artifact
303
392
  if: matrix.platform == 'linux-x86_64'
304
393
  uses: actions/upload-artifact@v7
305
394
  with:
306
395
  name: pypi-distributions
307
- path: ${{ steps.build.outputs.project_wheel_path }}
396
+ path: |
397
+ ${{ steps.build.outputs.project_wheel_path }}
398
+ ${{ steps.build.outputs.project_source_distribution_path }}
399
+
400
+ github-release:
401
+ name: Publish GitHub release assets
402
+ needs: [release_ref, validate, wheelhouse]
403
+ runs-on: ubuntu-24.04
404
+ permissions:
405
+ contents: write
406
+
407
+ steps:
408
+ - name: Download wheelhouse artifacts
409
+ uses: actions/download-artifact@v8
410
+ with:
411
+ pattern: src-auth-perms-sync-*
412
+ path: release-assets
413
+ merge-multiple: true
414
+
415
+ - name: Download release notes
416
+ uses: actions/download-artifact@v8
417
+ with:
418
+ name: release-notes
419
+ path: release-notes
308
420
 
309
421
  - name: Publish GitHub release assets
310
422
  env:
311
423
  GH_TOKEN: ${{ github.token }}
424
+ GH_REPO: ${{ github.repository }}
425
+ RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
312
426
  run: |
313
- release_tag="${{ steps.release.outputs.tag }}"
314
- asset_path="${{ steps.build.outputs.asset_path }}"
315
- checksum_path="${{ steps.build.outputs.checksum_path }}"
316
- project_wheel_path="${{ steps.build.outputs.project_wheel_path }}"
317
- notes_path="${{ steps.notes.outputs.path }}"
318
- release_assets=("${asset_path}" "${checksum_path}")
319
-
320
- if [[ "${{ matrix.platform }}" == "linux-x86_64" ]]; then
321
- release_assets+=("${project_wheel_path}")
427
+ release_tag="${RELEASE_TAG}"
428
+ notes_path="$(find release-notes -name release-notes.md -print -quit)"
429
+ mapfile -t release_assets < <(find release-assets -type f | sort)
430
+
431
+ if [[ -z "${notes_path}" ]]; then
432
+ echo "::error title=Missing release notes::release-notes.md was not found in release artifact."
433
+ exit 1
434
+ fi
435
+ if [[ "${#release_assets[@]}" -eq 0 ]]; then
436
+ echo "::error title=Missing release assets::No release assets were downloaded."
437
+ exit 1
322
438
  fi
323
439
 
324
440
  if gh release view "${release_tag}" >/dev/null 2>&1; then
@@ -334,7 +450,7 @@ jobs:
334
450
 
335
451
  pypi:
336
452
  name: Publish PyPI package
337
- needs: wheelhouse
453
+ needs: [validate, wheelhouse]
338
454
  runs-on: ubuntu-24.04
339
455
  permissions:
340
456
  contents: read
@@ -345,7 +461,7 @@ jobs:
345
461
 
346
462
  steps:
347
463
  - name: Download built distribution
348
- uses: actions/download-artifact@v7
464
+ uses: actions/download-artifact@v8
349
465
  with:
350
466
  name: pypi-distributions
351
467
  path: dist
@@ -354,3 +470,4 @@ jobs:
354
470
  uses: pypa/gh-action-pypi-publish@release/v1
355
471
  with:
356
472
  packages-dir: dist
473
+ skip-existing: true
@@ -0,0 +1,267 @@
1
+ name: Validate
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ ref:
7
+ description: "Git ref to validate. Defaults to the caller's ref."
8
+ required: false
9
+ type: string
10
+ build-package:
11
+ description: "Build and smoke-test package artifacts. Release builds do this separately."
12
+ required: false
13
+ type: boolean
14
+ default: true
15
+
16
+ permissions:
17
+ contents: read
18
+ pull-requests: read
19
+
20
+ defaults:
21
+ run:
22
+ shell: bash
23
+
24
+ jobs:
25
+ changes:
26
+ name: Detect changed paths
27
+ runs-on: ubuntu-24.04
28
+ outputs:
29
+ github_actions: ${{ steps.changed_paths.outputs.github_actions }}
30
+ markdown: ${{ steps.changed_paths.outputs.markdown }}
31
+ python: ${{ steps.changed_paths.outputs.python }}
32
+ package: ${{ steps.changed_paths.outputs.package }}
33
+
34
+ steps:
35
+ - name: Detect changed paths
36
+ id: changed_paths
37
+ env:
38
+ GH_TOKEN: ${{ github.token }}
39
+ PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
40
+ run: |
41
+ github_actions_changed=false
42
+ markdown_changed=false
43
+ python_changed=false
44
+ package_changed=false
45
+
46
+ if [[ "${{ github.event_name }}" != "pull_request" ]]; then
47
+ github_actions_changed=true
48
+ markdown_changed=true
49
+ python_changed=true
50
+ package_changed=true
51
+ else
52
+ changed_files="$(mktemp)"
53
+ gh api --paginate \
54
+ "repos/${GITHUB_REPOSITORY}/pulls/${PULL_REQUEST_NUMBER}/files" \
55
+ --jq '.[].filename' > "${changed_files}"
56
+
57
+ while IFS= read -r changed_file; do
58
+ case "${changed_file}" in
59
+ .github/workflows/*)
60
+ github_actions_changed=true
61
+ ;;
62
+ esac
63
+
64
+ case "${changed_file}" in
65
+ *.md|.markdownlint-cli2.yaml)
66
+ markdown_changed=true
67
+ ;;
68
+ esac
69
+
70
+ case "${changed_file}" in
71
+ .python-version|pyproject.toml|uv.lock|dev/*|src/*|tests/*)
72
+ python_changed=true
73
+ ;;
74
+ esac
75
+
76
+ case "${changed_file}" in
77
+ .python-version|LICENSE|README.md|maps-example.yaml|pyproject.toml|uv.lock|src/*)
78
+ package_changed=true
79
+ ;;
80
+ esac
81
+ done < "${changed_files}"
82
+ fi
83
+
84
+ {
85
+ echo "github_actions=${github_actions_changed}"
86
+ echo "markdown=${markdown_changed}"
87
+ echo "python=${python_changed}"
88
+ echo "package=${package_changed}"
89
+ } >> "${GITHUB_OUTPUT}"
90
+
91
+ github_actions:
92
+ name: Lint GitHub Actions
93
+ needs: changes
94
+ if: needs.changes.outputs.github_actions == 'true'
95
+ runs-on: ubuntu-24.04
96
+ env:
97
+ ACTIONLINT_VERSION: "1.7.12"
98
+
99
+ steps:
100
+ - name: Check out code
101
+ uses: actions/checkout@v6
102
+ with:
103
+ persist-credentials: false
104
+ ref: ${{ inputs.ref || github.ref }}
105
+
106
+ - name: Install actionlint
107
+ run: |
108
+ mkdir -p "${HOME}/.local/bin"
109
+ asset="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
110
+ checksums="actionlint_${ACTIONLINT_VERSION}_checksums.txt"
111
+ base_url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}"
112
+
113
+ curl -fsSLO "${base_url}/${asset}"
114
+ curl -fsSLO "${base_url}/${checksums}"
115
+ grep " ${asset}$" "${checksums}" | sha256sum --check
116
+ tar -xzf "${asset}" -C "${HOME}/.local/bin" actionlint
117
+ chmod 0755 "${HOME}/.local/bin/actionlint"
118
+
119
+ - name: Lint GitHub Actions
120
+ run: |
121
+ "${HOME}/.local/bin/actionlint"
122
+
123
+ markdown:
124
+ name: Lint Markdown
125
+ needs: changes
126
+ if: needs.changes.outputs.markdown == 'true'
127
+ runs-on: ubuntu-24.04
128
+ env:
129
+ MARKDOWNLINT_CLI2_VERSION: "0.22.1"
130
+
131
+ steps:
132
+ - name: Check out code
133
+ uses: actions/checkout@v6
134
+ with:
135
+ persist-credentials: false
136
+ ref: ${{ inputs.ref || github.ref }}
137
+
138
+ - name: Cache npm
139
+ uses: actions/cache@v5
140
+ with:
141
+ path: ~/.npm
142
+ key: npm-${{ runner.os }}-markdownlint-cli2-${{ env.MARKDOWNLINT_CLI2_VERSION }}
143
+
144
+ - name: Lint Markdown
145
+ run: npx --yes "markdownlint-cli2@${MARKDOWNLINT_CLI2_VERSION}"
146
+
147
+ python:
148
+ name: Validate Python
149
+ needs: changes
150
+ if: needs.changes.outputs.python == 'true'
151
+ runs-on: ubuntu-24.04
152
+ env:
153
+ PYTHON_VERSION: "3.11"
154
+ UV_VERSION: "0.11.7"
155
+
156
+ steps:
157
+ - name: Check out code
158
+ uses: actions/checkout@v6
159
+ with:
160
+ fetch-depth: 0
161
+ persist-credentials: false
162
+ ref: ${{ inputs.ref || github.ref }}
163
+
164
+ - name: Set up Python
165
+ uses: actions/setup-python@v6
166
+ with:
167
+ python-version: ${{ env.PYTHON_VERSION }}
168
+
169
+ - name: Cache uv
170
+ uses: actions/cache@v5
171
+ with:
172
+ path: ~/.cache/uv
173
+ key: uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock') }}
174
+ restore-keys: |
175
+ uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-
176
+
177
+ - name: Install uv
178
+ run: python -m pip install "uv==${UV_VERSION}"
179
+
180
+ - name: Validate lockfile
181
+ run: uv lock --check
182
+
183
+ - name: Lint Python
184
+ run: uv run --frozen ruff check .
185
+
186
+ - name: Check Python formatting
187
+ run: uv run --frozen ruff format --check .
188
+
189
+ - name: Type check
190
+ run: uv run --frozen pyright
191
+
192
+ - name: Run tests
193
+ run: uv run --frozen python -m unittest discover -s tests
194
+
195
+ - name: Smoke test source checkout CLI
196
+ run: uv run --frozen src-auth-perms-sync --help >/tmp/src-auth-perms-sync-help.txt
197
+
198
+ package_build:
199
+ name: Build and smoke-test package
200
+ needs: changes
201
+ if: inputs.build-package && needs.changes.outputs.package == 'true'
202
+ runs-on: ubuntu-24.04
203
+ env:
204
+ PACKAGE_NAME: src-auth-perms-sync
205
+ PYTHON_VERSION: "3.11"
206
+ UV_VERSION: "0.11.7"
207
+
208
+ steps:
209
+ - name: Check out code
210
+ uses: actions/checkout@v6
211
+ with:
212
+ fetch-depth: 0
213
+ persist-credentials: false
214
+ ref: ${{ inputs.ref || github.ref }}
215
+
216
+ - name: Set up Python
217
+ uses: actions/setup-python@v6
218
+ with:
219
+ python-version: ${{ env.PYTHON_VERSION }}
220
+
221
+ - name: Cache uv
222
+ uses: actions/cache@v5
223
+ with:
224
+ path: ~/.cache/uv
225
+ key: uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock') }}
226
+ restore-keys: |
227
+ uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-
228
+
229
+ - name: Install uv
230
+ run: python -m pip install "uv==${UV_VERSION}"
231
+
232
+ - name: Build distributions
233
+ run: uv build --wheel --sdist --out-dir dist --no-create-gitignore
234
+
235
+ - name: Smoke test installed wheel
236
+ run: |
237
+ python -m venv build/ci-venv
238
+ . build/ci-venv/bin/activate
239
+ python -m pip install dist/src_auth_perms_sync-*.whl
240
+ src-auth-perms-sync --help >/tmp/src-auth-perms-sync-installed-help.txt
241
+ python -m src_auth_perms_sync --help >/tmp/src-auth-perms-sync-module-help.txt
242
+
243
+ package:
244
+ name: Validate package
245
+ needs: [changes, github_actions, markdown, python, package_build]
246
+ if: always()
247
+ runs-on: ubuntu-24.04
248
+
249
+ steps:
250
+ - name: Confirm validation results
251
+ run: |
252
+ for validation_result in \
253
+ "${{ needs.changes.result }}" \
254
+ "${{ needs.github_actions.result }}" \
255
+ "${{ needs.markdown.result }}" \
256
+ "${{ needs.python.result }}" \
257
+ "${{ needs.package_build.result }}"
258
+ do
259
+ case "${validation_result}" in
260
+ success|skipped)
261
+ ;;
262
+ *)
263
+ echo "::error title=Validation failed::At least one validation job ended with '${validation_result}'."
264
+ exit 1
265
+ ;;
266
+ esac
267
+ done