Skip to content

Commit ea92442

Browse files
Copilotnstarman
andauthored
Harden CD by adopting unxt's secure staged-publish model; unify Stage B into single workflow (#496)
* chore: start CD security hardening plan * harden CD workflows to follow unxt staged publish model * unify five cd-publish workflows into single cd-publish.yml Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nstarman <8949649+nstarman@users.noreply.github.com>
1 parent 843c1f9 commit ea92442

8 files changed

Lines changed: 667 additions & 419 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Dispatch package CD workflow
2+
description: >
3+
Dispatch a package CD workflow for a tag, with an idempotency check so that
4+
re-running the coordinator workflow does not trigger duplicate CD runs.
5+
6+
inputs:
7+
workflow_id:
8+
description: The workflow file name to dispatch (for example cd-coordinax.yml).
9+
required: true
10+
workflow_name:
11+
description: Human-readable workflow name for log messages.
12+
required: true
13+
tag_ref:
14+
description: The tag ref to dispatch the workflow on (for example coordinax-v1.2.0).
15+
required: true
16+
tag_sha:
17+
description: The commit SHA the tag points to.
18+
required: true
19+
trigger_cd:
20+
description: >
21+
Whether a new tag was just created ('true') or all tags pre-existed
22+
('false', i.e. a re-run). When 'false', an idempotency check is performed
23+
before dispatching.
24+
required: true
25+
26+
runs:
27+
using: composite
28+
steps:
29+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
30+
with:
31+
script: |
32+
const workflowId = "${{ inputs.workflow_id }}";
33+
const workflowName = "${{ inputs.workflow_name }}";
34+
const tagRef = "${{ inputs.tag_ref }}";
35+
const tagSha = "${{ inputs.tag_sha }}";
36+
const shouldDispatch = "${{ inputs.trigger_cd }}" === "true";
37+
38+
if (!shouldDispatch) {
39+
const existingRuns = await github.paginate(
40+
github.rest.actions.listWorkflowRuns,
41+
{
42+
owner: context.repo.owner,
43+
repo: context.repo.repo,
44+
workflow_id: workflowId,
45+
branch: tagRef,
46+
per_page: 100,
47+
},
48+
);
49+
50+
const hasMatchingRun = existingRuns.some(
51+
(run) => {
52+
const isMatchingTagSha =
53+
run.head_branch === tagRef && run.head_sha === tagSha;
54+
const isBlockingRun =
55+
run.status !== "completed" ||
56+
(run.status === "completed" && run.conclusion === "success");
57+
58+
return isMatchingTagSha && isBlockingRun;
59+
},
60+
);
61+
62+
if (hasMatchingRun) {
63+
core.info(
64+
`Skipping CD dispatch for ${tagRef}: a queued/in-progress/successful ${workflowName} run already exists for ${tagSha}.`,
65+
);
66+
return;
67+
}
68+
69+
core.info(
70+
`No queued/in-progress/successful ${workflowName} run found for ${tagRef} at ${tagSha}; dispatching recovery run.`,
71+
);
72+
}
73+
74+
try {
75+
await github.rest.actions.createWorkflowDispatch({
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
workflow_id: workflowId,
79+
ref: tagRef,
80+
});
81+
core.info(`Triggered ${workflowName} for tag ${tagRef}`);
82+
} catch (error) {
83+
core.setFailed(
84+
`Failed to trigger ${workflowName} for tag ${tagRef}: ${error.message}`,
85+
);
86+
}

.github/workflows/cd-coordinax-api.yml

Lines changed: 37 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ name: CD - coordinax.api
22

33
on:
44
workflow_dispatch:
5-
pull_request:
6-
paths:
7-
- "packages/coordinax.api/**"
8-
- ".github/workflows/cd-coordinax.api.yml"
95
push:
106
branches:
117
- main
@@ -14,11 +10,6 @@ on:
1410
paths:
1511
- "packages/coordinax.api/**"
1612
- ".github/workflows/cd-coordinax-api.yml"
17-
workflow_run:
18-
workflows:
19-
- "Create Package Tags"
20-
types:
21-
- completed
2213

2314
concurrency:
2415
group: ${{ github.workflow }}-${{ github.ref }}
@@ -33,14 +24,9 @@ jobs:
3324
runs-on: ubuntu-latest
3425
permissions:
3526
contents: read
36-
id-token: write
37-
attestations: write
38-
# Build runs for direct tag pushes or via workflow_run from Create Package Tags
39-
# (which handles coordinator tag releases)
27+
# Build runs for manual dispatch, main pushes, and direct package tag pushes.
4028
if: |
4129
github.event_name == 'workflow_dispatch' ||
42-
github.event_name == 'pull_request' ||
43-
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') ||
4430
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
4531
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
4632
@@ -51,83 +37,52 @@ jobs:
5137
# Can be removed if switching to hardcoded versions.
5238
fetch-depth: 0
5339
persist-credentials: false
54-
# For workflow_run events, checkout the exact commit from upstream workflow
55-
ref: >-
56-
${{ github.event_name == 'workflow_run' &&
57-
github.event.workflow_run.head_sha || github.ref }}
58-
59-
# For workflow_run events, find and export the package-specific tag
60-
- name: Find package tag (workflow_run)
61-
if: github.event_name == 'workflow_run'
62-
uses: ./.github/actions/find-package-tag
63-
with:
64-
tag_glob: coordinax-api-v*
65-
commit_sha: ${{ github.event.workflow_run.head_sha }}
66-
67-
- uses: astral-sh/setup-uv@v7
40+
# This workflow only runs on trusted `push` and `workflow_dispatch` events.
41+
ref: ${{ github.ref }}
6842

6943
- name: Validate git tag (for tagged releases)
70-
if: |
71-
startsWith(github.ref, 'refs/tags/') || github.event_name ==
72-
'workflow_run'
44+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
45+
env:
46+
GITHUB_REF_VAR: ${{ github.ref }}
47+
run: |
48+
TAG="${GITHUB_REF_VAR#refs/tags/}"
49+
python scripts/validate_tag.py "$TAG" "coordinax.api"
50+
shell: bash
51+
52+
- name: Emit release metadata
53+
env:
54+
EVENT_NAME: ${{ github.event_name }}
7355
run: |
74-
if [ "${{ github.event_name }}" = "workflow_run" ]; then
75-
TAG="$PACKAGE_TAG"
56+
HEAD_SHA=$(git rev-parse HEAD)
57+
58+
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
59+
TAG="${GITHUB_REF#refs/tags/}"
7660
else
77-
TAG=${GITHUB_REF#refs/tags/}
61+
TAG=""
7862
fi
79-
uv run scripts/validate_tag.py "$TAG" "coordinax.api"
63+
TRIGGER_EVENT="${EVENT_NAME}"
64+
65+
jq -n \
66+
--arg package "coordinax.api" \
67+
--arg package_tag "${TAG}" \
68+
--arg head_sha "${HEAD_SHA}" \
69+
--arg trigger_event "${TRIGGER_EVENT}" \
70+
'{"package": $package, "package_tag": $package_tag, "head_sha": $head_sha, "trigger_event": $trigger_event}' \
71+
> release-metadata.json
72+
73+
echo "Release metadata:"
74+
cat release-metadata.json
8075
shell: bash
8176

8277
- uses: hynek/build-and-inspect-python-package@fe0a0fb1925ca263d076ca4f2c13e93a6e92a33e # v2.17.0
8378
with:
8479
path: packages/coordinax.api
8580
upload-name-suffix: -coordinax.api
86-
attest-build-provenance-github: ${{ github.event_name != 'pull_request' }}
81+
attest-build-provenance-github: false
8782

88-
publish-testpypi:
89-
name: Publish to TestPyPI
90-
needs: [build]
91-
runs-on: ubuntu-latest
92-
environment:
93-
name: testpypi
94-
url: https://test.pypi.org/p/coordinax.api
95-
permissions:
96-
id-token: write
97-
if: |
98-
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
99-
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/coordinax-api-v')) ||
100-
github.event_name == 'workflow_run'
101-
102-
steps:
103-
- name: Download built artifact to dist/
104-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
83+
- name: Upload release metadata artifact
84+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
10585
with:
106-
name: Packages-coordinax.api
107-
path: dist
108-
109-
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
110-
with:
111-
repository-url: https://test.pypi.org/legacy/
112-
113-
publish-pypi:
114-
name: Publish to PyPI
115-
needs: [build]
116-
runs-on: ubuntu-latest
117-
environment:
118-
name: pypi
119-
url: https://pypi.org/p/coordinax.api
120-
permissions:
121-
id-token: write
122-
if: |
123-
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/coordinax-api-v')) ||
124-
github.event_name == 'workflow_run'
125-
126-
steps:
127-
- name: Download built artifact to dist/
128-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
129-
with:
130-
name: Packages-coordinax.api
131-
path: dist
132-
133-
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
86+
name: release-metadata-coordinax-api
87+
path: release-metadata.json
88+
retention-days: 7

0 commit comments

Comments
 (0)