-
Notifications
You must be signed in to change notification settings - Fork 0
202 lines (177 loc) · 7.83 KB
/
outbound-bridge.yml
File metadata and controls
202 lines (177 loc) · 7.83 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
# Digital Service Orchestra — Outbound Bridge
# Pushes local ticket changes to Jira when the tickets branch is updated.
name: Outbound Bridge
on:
# Push trigger removed (71fa-c068): GitHub Actions does not detect workflow
# files on orphan branches, so push-to-tickets never fired (all 100 recent
# runs were workflow_dispatch). merge-to-main.sh dispatches explicitly.
workflow_dispatch: {}
concurrency:
group: jira-bridge
cancel-in-progress: false
jobs:
bridge:
name: Push tickets to Jira
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
# Skip commits made by the bridge bot itself (echo prevention).
# Fall back to 'dso-bridge[bot]' when BRIDGE_BOT_LOGIN is unset so the
# guard is never silently bypassed (unset var would evaluate to '' and
# always pass, causing an infinite push loop on the tickets branch).
if: github.actor != (vars.BRIDGE_BOT_LOGIN || 'dso-bridge[bot]')
steps:
- name: Checkout main (scripts and plugin code)
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: main
- name: Mount tickets branch as worktree
run: |
git fetch origin tickets --depth=2
rm -rf .tickets-tracker
git worktree add .tickets-tracker tickets
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Cache ACLI binary
id: cache-acli
uses: actions/cache@v4
with:
path: ~/.acli
key: acli-${{ runner.os }}-${{ vars.ACLI_VERSION || 'latest' }}
- name: Validate ACLI version before download
if: steps.cache-acli.outputs.cache-hit != 'true'
run: |
ACLI_VERSION="${{ vars.ACLI_VERSION }}"
if [ -z "$ACLI_VERSION" ] || [ "$ACLI_VERSION" = "latest" ]; then
echo "ERROR: ACLI_VERSION is unset or 'latest' — cannot proceed with download."
echo " Pin ACLI_VERSION to a specific release and set ACLI_SHA256 to its"
echo " expected SHA256 digest before running this workflow."
exit 1
fi
- name: Download ACLI
if: steps.cache-acli.outputs.cache-hit != 'true'
run: |
mkdir -p ~/.acli
# ACLI v1.3+ is a Go binary distributed as tar.gz from acli.atlassian.com.
curl -sSL "https://acli.atlassian.com/linux/${{ vars.ACLI_VERSION }}/acli_${{ vars.ACLI_VERSION }}_linux_amd64.tar.gz" \
-o ~/.acli/acli.tar.gz
- name: Verify ACLI checksum
if: steps.cache-acli.outputs.cache-hit != 'true'
run: |
ACLI_VERSION="${{ vars.ACLI_VERSION }}"
ACLI_SHA256="${{ vars.ACLI_SHA256 }}"
ARTIFACT=~/.acli/acli.tar.gz
if [ -z "$ACLI_VERSION" ] || [ "$ACLI_VERSION" = "latest" ]; then
echo "ERROR: ACLI_VERSION is unset or 'latest' — checksum verification cannot proceed."
echo " Pin ACLI_VERSION to a specific release and set ACLI_SHA256 to its expected"
echo " SHA256 digest before running this workflow."
exit 1
elif [ -z "$ACLI_SHA256" ]; then
COMPUTED_HASH=$(sha256sum "$ARTIFACT" | awk '{print $1}')
echo "WARNING: ACLI_SHA256 is not set. Computed SHA256 of downloaded artifact:"
echo ""
echo " ACLI_SHA256=${COMPUTED_HASH}"
echo ""
echo "Set this value as a GitHub repository variable:"
echo " gh variable set ACLI_SHA256 --body '${COMPUTED_HASH}'"
echo ""
echo "Proceeding WITHOUT checksum verification (first-run bootstrap)."
else
echo "${ACLI_SHA256} ${ARTIFACT}" | sha256sum -c --strict
echo "Checksum verified: ${ARTIFACT}"
fi
- name: Extract ACLI binary
if: steps.cache-acli.outputs.cache-hit != 'true'
run: |
# ACLI v1.3+ is a Go binary in a tar.gz with a version-prefixed directory.
# --strip-components=1 removes the top-level dir (e.g., acli_1.3.14-stable_linux_amd64/).
tar xzf ~/.acli/acli.tar.gz -C ~/.acli/ --strip-components=1
chmod +x ~/.acli/acli
rm -f ~/.acli/acli.tar.gz
echo "Extracted acli binary from tar.gz."
- name: Add ACLI to PATH
run: |
if [ ! -f "$HOME/.acli/acli" ]; then
echo "ERROR: $HOME/.acli/acli not found — download or extraction must have failed." >&2
exit 1
fi
ln -sf "$HOME/.acli/acli" /usr/local/bin/acli
- name: Authenticate ACLI
run: |
echo "$JIRA_API_TOKEN" | acli jira auth login \
--site "$JIRA_URL" \
--email "$JIRA_USER" \
--token
env:
JIRA_URL: ${{ vars.JIRA_URL }}
JIRA_USER: ${{ vars.JIRA_USER }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
- name: Run outbound bridge
id: run-bridge
run: |
python3 plugins/dso/scripts/bridge-outbound.py
env:
JIRA_URL: ${{ vars.JIRA_URL }}
JIRA_USER: ${{ vars.JIRA_USER }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_PROJECT: ${{ vars.JIRA_PROJECT }}
BRIDGE_ENV_ID: ${{ vars.BRIDGE_ENV_ID }}
GH_RUN_ID: ${{ github.run_id }}
- name: Commit SYNC events back to tickets branch
run: |
cd .tickets-tracker
if [ -z "$(git status --porcelain)" ]; then
echo "No SYNC events to commit — skipping."
exit 0
fi
# Configure bridge bot identity (not the default github-actions[bot]).
# Values are read from env vars (set via the env: map below) rather than
# interpolated directly into the run: block to prevent expression injection.
git config user.name "${BRIDGE_BOT_NAME}"
git config user.email "${BRIDGE_BOT_EMAIL}"
git add -A
git commit -m "chore: sync SYNC events from Jira bridge [run ${{ github.run_id }}]"
# Push with 3-retry fetch-rebase-push loop.
# The tickets branch may be updated by merge-to-main.sh or ticket-lifecycle
# during the bridge run, causing a non-fast-forward rejection.
# Pattern: ticket-lifecycle.sh Step 5.
max_retries=3
attempt=0
while [ "$attempt" -lt "$max_retries" ]; do
attempt=$((attempt + 1))
push_stderr=""
push_exit=0
push_stderr=$(git push origin HEAD:tickets 2>&1) || push_exit=$?
if [ "$push_exit" -eq 0 ]; then
echo "Push succeeded (attempt $attempt)."
exit 0
fi
# Check if failure is retryable (non-fast-forward) vs fatal
if echo "$push_stderr" | grep -qiE 'non-fast-forward|rejected|fetch first'; then
echo "Push rejected (attempt $attempt) — fetching and rebasing before retry..."
git fetch origin tickets 2>/dev/null || true
else
echo "Push failed (non-retryable, exit $push_exit): $push_stderr" >&2
exit 1
fi
rebase_exit=0
git rebase origin/tickets 2>/dev/null || rebase_exit=$?
if [ "$rebase_exit" -ne 0 ]; then
git rebase --abort 2>/dev/null || true
echo "Rebase conflict during push retry (attempt $attempt)" >&2
exit 1
fi
done
echo "Push failed after $max_retries retries" >&2
exit 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRIDGE_BOT_NAME: ${{ vars.BRIDGE_BOT_NAME || 'dso-bridge[bot]' }}
BRIDGE_BOT_EMAIL: ${{ vars.BRIDGE_BOT_EMAIL || 'dso-bridge@users.noreply.github.com' }}
- name: Job timing report
if: always()
run: echo "outbound-bridge completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)"