Inbound Bridge #206
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Digital Service Orchestra — Inbound Bridge | |
| # Pulls Jira changes into local ticket system on a scheduled interval. | |
| name: Inbound Bridge | |
| on: | |
| schedule: | |
| - cron: '*/30 * * * *' | |
| workflow_dispatch: {} | |
| concurrency: | |
| group: jira-bridge | |
| cancel-in-progress: false | |
| jobs: | |
| bridge: | |
| name: Pull tickets from 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=1 | |
| # Remove the placeholder .tickets-tracker/ from main (if it exists) | |
| 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: | | |
| # Reject unset or unpinned version strings BEFORE any download occurs so | |
| # no untrusted artifact is fetched with an interpolated/attacker-controlled | |
| # version path. | |
| 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. | |
| # The runner is linux/amd64 (ubuntu-latest). | |
| 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: | | |
| # Verify the downloaded artifact BEFORE extraction so a compromised | |
| # or MITM'd download cannot execute arbitrary code. | |
| 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 | |
| # Compute and log the hash so the operator can capture it from CI output | |
| 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: | | |
| # ACLI Go binary requires explicit auth login (no env var auto-detection). | |
| # Pipe the API token to stdin via --token flag. | |
| 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 inbound bridge | |
| id: run-bridge | |
| run: | | |
| python3 plugins/dso/scripts/bridge-inbound.py | |
| env: | |
| JIRA_URL: ${{ vars.JIRA_URL }} | |
| JIRA_USER: ${{ vars.JIRA_USER }} | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| BRIDGE_ENV_ID: ${{ vars.BRIDGE_ENV_ID }} | |
| GH_RUN_ID: ${{ github.run_id }} | |
| INBOUND_CHECKPOINT_PATH: ${{ vars.INBOUND_CHECKPOINT_PATH || '.tickets-tracker/.inbound-checkpoint.json' }} | |
| INBOUND_OVERLAP_BUFFER_MINUTES: ${{ vars.INBOUND_OVERLAP_BUFFER_MINUTES || '15' }} | |
| INBOUND_STATUS_MAPPING: ${{ vars.INBOUND_STATUS_MAPPING || '{}' }} | |
| INBOUND_TYPE_MAPPING: ${{ vars.INBOUND_TYPE_MAPPING || '{}' }} | |
| JIRA_PROJECT: ${{ vars.JIRA_PROJECT }} | |
| - name: Commit CREATE events back to tickets branch | |
| run: | | |
| cd .tickets-tracker | |
| # Check for any changes (staged, unstaged, or untracked) | |
| if [ -z "$(git status --porcelain)" ]; then | |
| echo "No new 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 CREATE events from Jira inbound bridge [run ${{ github.run_id }}]" | |
| git push origin HEAD:tickets | |
| 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 "inbound-bridge completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)" |