Release Conductor #16248
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
| name: Release Conductor | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| apply: | |
| description: 'Apply mode (requires RELEASE_CONDUCTOR_ENABLED=1)' | |
| required: false | |
| default: false | |
| type: boolean | |
| repair_existing_tag: | |
| description: 'Repair an existing authoritative tag or dispatch protected-tag-safe replay for immutable published tags' | |
| required: false | |
| default: false | |
| type: boolean | |
| channel: | |
| description: 'Release channel' | |
| required: false | |
| default: stable | |
| type: choice | |
| options: | |
| - stable | |
| - rc | |
| version: | |
| description: 'Release version proposal (for example 0.8.0 or 0.8.0-rc.1)' | |
| required: false | |
| type: string | |
| quarantine_stale_hours: | |
| description: 'Quarantine stale threshold hours (default 24)' | |
| required: false | |
| type: string | |
| schedule: | |
| - cron: '*/15 * * * *' | |
| workflow_run: | |
| workflows: | |
| - Queue Supervisor | |
| types: | |
| - completed | |
| branches: | |
| - develop | |
| concurrency: | |
| group: release-conductor-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| release-conductor: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Prepare release conductor token | |
| shell: pwsh | |
| env: | |
| INPUT_TOKEN_PRIMARY: ${{ secrets.GH_TOKEN }} | |
| INPUT_TOKEN_SECONDARY: ${{ secrets.GITHUB_TOKEN }} | |
| INPUT_TOKEN_TERTIARY: ${{ github.token }} | |
| run: | | |
| pwsh -NoLogo -NoProfile -File tools/priority/Resolve-PolicyToken.ps1 -TokenFileName release-conductor-gh-token.txt | |
| - name: Configure release tag signing material | |
| shell: bash | |
| env: | |
| RELEASE_TAG_SIGNING_PRIVATE_KEY: ${{ secrets.RELEASE_TAG_SIGNING_PRIVATE_KEY }} | |
| RELEASE_TAG_SIGNING_PUBLIC_KEY: ${{ secrets.RELEASE_TAG_SIGNING_PUBLIC_KEY }} | |
| RELEASE_TAG_SIGNING_IDENTITY_NAME: ${{ vars.RELEASE_TAG_SIGNING_IDENTITY_NAME || '' }} | |
| RELEASE_TAG_SIGNING_IDENTITY_EMAIL: ${{ vars.RELEASE_TAG_SIGNING_IDENTITY_EMAIL || '' }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${RELEASE_TAG_SIGNING_PRIVATE_KEY:-}" ]]; then | |
| echo "No release tag signing key configured; skipping workflow-owned signing setup." | |
| exit 0 | |
| fi | |
| if [[ -z "${GH_TOKEN:-}" ]]; then | |
| echo "::error::GH_TOKEN is unavailable after Resolve-PolicyToken; cannot derive workflow signing identity." | |
| exit 1 | |
| fi | |
| signing_dir="$RUNNER_TEMP/release-tag-signing" | |
| mkdir -p "$signing_dir" | |
| private_key_path="$signing_dir/id_release_tag_signing" | |
| public_key_path="${private_key_path}.pub" | |
| signing_login="$(gh api user --jq '.login')" | |
| signing_id="$(gh api user --jq '.id')" | |
| signing_name="${RELEASE_TAG_SIGNING_IDENTITY_NAME:-}" | |
| signing_email="${RELEASE_TAG_SIGNING_IDENTITY_EMAIL:-}" | |
| if [[ -z "$signing_name" ]]; then | |
| signing_name="$(gh api user --jq '.name // .login')" | |
| fi | |
| if [[ -z "$signing_email" ]]; then | |
| signing_email="$(gh api user --jq '.email // ""')" | |
| fi | |
| if [[ -z "$signing_email" ]]; then | |
| signing_email="${signing_id}+${signing_login}@users.noreply.github.com" | |
| fi | |
| identity_source="policy-token-user" | |
| if [[ -n "${RELEASE_TAG_SIGNING_IDENTITY_NAME:-}" || -n "${RELEASE_TAG_SIGNING_IDENTITY_EMAIL:-}" ]]; then | |
| identity_source="repo-variable-override" | |
| fi | |
| printf '%s\n' "$RELEASE_TAG_SIGNING_PRIVATE_KEY" > "$private_key_path" | |
| chmod 600 "$private_key_path" | |
| if [[ -n "${RELEASE_TAG_SIGNING_PUBLIC_KEY:-}" ]]; then | |
| printf '%s\n' "$RELEASE_TAG_SIGNING_PUBLIC_KEY" > "$public_key_path" | |
| else | |
| ssh-keygen -y -f "$private_key_path" > "$public_key_path" | |
| fi | |
| chmod 644 "$public_key_path" | |
| git config gpg.format ssh | |
| git config user.signingkey "$public_key_path" | |
| git config user.name "$signing_name" | |
| git config user.email "$signing_email" | |
| git config tag.gpgSign true | |
| { | |
| echo "RELEASE_TAG_SIGNING_BACKEND=ssh" | |
| echo "RELEASE_TAG_SIGNING_SOURCE=workflow-secret" | |
| echo "RELEASE_TAG_SIGNING_IDENTITY_NAME=$signing_name" | |
| echo "RELEASE_TAG_SIGNING_IDENTITY_EMAIL=$signing_email" | |
| echo "RELEASE_TAG_SIGNING_IDENTITY_LOGIN=$signing_login" | |
| echo "RELEASE_TAG_SIGNING_IDENTITY_ID=$signing_id" | |
| echo "RELEASE_TAG_SIGNING_IDENTITY_SOURCE=$identity_source" | |
| } >> "$GITHUB_ENV" | |
| - name: Run release conductor | |
| shell: pwsh | |
| env: | |
| RELEASE_CONDUCTOR_ENABLED: ${{ vars.RELEASE_CONDUCTOR_ENABLED || '0' }} | |
| RELEASE_TAG_SIGNING_BACKEND: ${{ env.RELEASE_TAG_SIGNING_BACKEND || '' }} | |
| RELEASE_TAG_SIGNING_SOURCE: ${{ env.RELEASE_TAG_SIGNING_SOURCE || '' }} | |
| run: | | |
| npm ci --ignore-scripts | |
| node tools/npm/run-script.mjs priority:queue:supervisor -- --dry-run --report tests/results/_agent/queue/queue-supervisor-report.json | |
| node tools/npm/run-script.mjs priority:policy:snapshot -- --output tests/results/_agent/policy/policy-state-snapshot.json | |
| $reportPath = 'tests/results/_agent/release/release-conductor-report.json' | |
| $args = @( | |
| 'tools/npm/run-script.mjs', | |
| 'priority:release:conductor', | |
| '--', | |
| '--report', | |
| $reportPath, | |
| '--queue-report', | |
| 'tests/results/_agent/queue/queue-supervisor-report.json', | |
| '--policy-snapshot', | |
| 'tests/results/_agent/policy/policy-state-snapshot.json' | |
| ) | |
| $eventName = '${{ github.event_name }}' | |
| $apply = $false | |
| $conductorEnabled = ($env:RELEASE_CONDUCTOR_ENABLED -eq '1') | |
| if ($eventName -eq 'workflow_dispatch') { | |
| $apply = ('${{ inputs.apply }}' -eq 'true') | |
| } elseif ($eventName -eq 'workflow_run') { | |
| $apply = $false | |
| if (-not $conductorEnabled) { | |
| Write-Host 'Release conductor apply mode disabled; workflow_run will remain proposal-only.' | |
| } else { | |
| Write-Host 'Release conductor workflow_run does not carry explicit release version inputs; running proposal-only.' | |
| } | |
| } | |
| if ($apply) { | |
| $args += '--apply' | |
| } else { | |
| $args += '--dry-run' | |
| } | |
| if ('${{ inputs.repair_existing_tag }}' -eq 'true') { | |
| $args += '--repair-existing-tag' | |
| } | |
| $channelInput = '${{ inputs.channel }}' | |
| if (-not [string]::IsNullOrWhiteSpace($channelInput)) { | |
| $args += @('--channel', $channelInput.Trim().ToLowerInvariant()) | |
| } | |
| $versionInput = '${{ inputs.version }}' | |
| if (-not [string]::IsNullOrWhiteSpace($versionInput)) { | |
| $args += @('--version', $versionInput.Trim()) | |
| } | |
| $quarantineInput = '${{ inputs.quarantine_stale_hours }}' | |
| if (-not [string]::IsNullOrWhiteSpace($quarantineInput)) { | |
| $args += @('--quarantine-stale-hours', $quarantineInput.Trim()) | |
| } | |
| node @args | |
| - name: Upload release conductor artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-conductor-${{ github.run_id }} | |
| path: | | |
| tests/results/_agent/release/release-conductor-report.json | |
| tests/results/_agent/queue/queue-supervisor-report.json | |
| tests/results/_agent/policy/policy-state-snapshot.json | |
| if-no-files-found: error |