feat(ci): overhaul CI/CD with smart detection & optimized caching #1
Workflow file for this run
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
# .github/workflows/_publish.yml | ||
name: _publish | ||
on: | ||
workflow_call: | ||
inputs: | ||
dry_run: | ||
type: boolean | ||
required: false | ||
default: false | ||
description: "Run in dry-run mode" | ||
permissions: | ||
contents: read | ||
packages: write | ||
id-token: write | ||
concurrency: | ||
group: publish-${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: false | ||
env: | ||
IGGY_CI_BUILD: true | ||
jobs: | ||
parse: | ||
name: Parse tag | ||
runs-on: ubuntu-latest | ||
outputs: | ||
matched: ${{ steps.p.outputs.matched }} | ||
version: ${{ steps.p.outputs.version }} | ||
targets: ${{ steps.p.outputs.targets }} | ||
valid: ${{ steps.p.outputs.valid }} | ||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v4 | ||
- name: Load publish config | ||
id: config | ||
run: | | ||
if ! command -v yq &> /dev/null; then | ||
wget -qO /tmp/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 | ||
chmod +x /tmp/yq | ||
sudo mv /tmp/yq /usr/local/bin/yq | ||
fi | ||
# NOTE: this now reads the `.publish` root from config | ||
echo "publish_config=$(yq -o=json -I=0 '.publish' .github/config/publish.yml | jq -c)" >> $GITHUB_OUTPUT | ||
- name: Parse tag and determine targets | ||
id: p | ||
uses: actions/github-script@v7 | ||
with: | ||
script: | | ||
const ref = context.ref; | ||
const tag = ref.split('/').pop(); | ||
core.info(`Processing tag: ${tag}`); | ||
let config = {}; | ||
try { | ||
config = JSON.parse(`${{ steps.config.outputs.publish_config }}`) || {}; | ||
} catch (e) { | ||
core.setFailed('Invalid publish configuration'); | ||
core.setOutput('valid', 'false'); | ||
return; | ||
} | ||
for (const [name, cfg] of Object.entries(config)) { | ||
const re = new RegExp(cfg.tag_pattern); | ||
const m = tag.match(re); | ||
if (!m) continue; | ||
const version = (m[1] || '').trim(); | ||
if (!version) { | ||
core.setFailed(`Matched ${name} but failed to extract version from tag using capture group 1`); | ||
core.setOutput('valid', 'false'); | ||
return; | ||
} | ||
core.info(`✓ Matched: ${name}, Version: ${version}`); | ||
core.setOutput('matched', name); | ||
core.setOutput('version', version); | ||
core.setOutput('targets', JSON.stringify(cfg.targets || [])); | ||
core.setOutput('valid', 'true'); | ||
return; | ||
} | ||
core.setFailed(`Tag ${tag} didn't match any publish pattern`); | ||
core.setOutput('valid', 'false'); | ||
publish: | ||
name: Publish ${{ matrix.target.type }} | ||
needs: parse | ||
if: needs.parse.outputs.valid == 'true' | ||
runs-on: ubuntu-latest | ||
environment: release | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
target: ${{ fromJson(needs.parse.outputs.targets) }} | ||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v4 | ||
- name: Publish (Docker) | ||
if: matrix.target.type == 'docker-buildx' | ||
uses: ./.github/actions/docker-buildx | ||
with: | ||
component: ${{ matrix.target.component || '' }} | ||
image: ${{ matrix.target.image || '' }} | ||
dockerfile:${{ matrix.target.dockerfile || '' }} | ||
version: ${{ needs.parse.outputs.version }} | ||
push: ${{ !inputs.dry_run }} | ||
- name: Publish (Rust crates) | ||
if: matrix.target.type == 'rust' | ||
uses: ./.github/actions/rust | ||
with: | ||
task: publish | ||
package: ${{ matrix.target.package }} | ||
version: ${{ needs.parse.outputs.version }} | ||
dry_run: ${{ inputs.dry_run }} | ||
- name: Publish (Python) | ||
if: matrix.target.type == 'python-maturin' | ||
uses: ./.github/actions/python-maturin | ||
with: | ||
task: publish | ||
version: ${{ needs.parse.outputs.version }} | ||
dry_run: ${{ inputs.dry_run }} | ||
- name: Publish (Node) | ||
if: matrix.target.type == 'node-npm' | ||
uses: ./.github/actions/node-npm | ||
with: | ||
task: publish | ||
version: ${{ needs.parse.outputs.version }} | ||
dry_run: ${{ inputs.dry_run }} | ||
- name: Publish (Java) | ||
if: matrix.target.type == 'java-gradle' | ||
uses: ./.github/actions/java-gradle | ||
with: | ||
task: publish | ||
version: ${{ needs.parse.outputs.version }} | ||
dry_run: ${{ inputs.dry_run }} | ||
- name: Publish (C#) | ||
if: matrix.target.type == 'csharp-dotnet' | ||
uses: ./.github/actions/csharp-dotnet | ||
with: | ||
task: publish | ||
version: ${{ needs.parse.outputs.version }} | ||
dry_run: ${{ inputs.dry_run }} | ||
- name: Publish (Go) | ||
if: matrix.target.type == 'go' | ||
run: | | ||
echo "New go tag detected: v${{ needs.parse.outputs.version }} (no publish action required)" | ||
env: | ||
# Rust | ||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | ||
# Docker Hub | ||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | ||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | ||
# PyPI | ||
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | ||
# npm | ||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
# Nexus | ||
NEXUS_USER: ${{ secrets.NEXUS_USER }} | ||
NEXUS_PASSWORD: ${{ secrets.NEXUS_PW }} | ||
# NuGet | ||
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} | ||
summary: | ||
name: Publish Summary | ||
needs: [parse, publish] | ||
if: always() | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Summary | ||
run: | | ||
echo "# 📦 Publish Summary" >> $GITHUB_STEP_SUMMARY | ||
echo "" >> $GITHUB_STEP_SUMMARY | ||
if [ "${{ needs.parse.outputs.valid }}" != "true" ]; then | ||
echo "❌ Invalid tag - no matching publish pattern" >> $GITHUB_STEP_SUMMARY | ||
exit 1 | ||
fi | ||
echo "- **Component**: ${{ needs.parse.outputs.matched }}" >> $GITHUB_STEP_SUMMARY | ||
echo "- **Version**: ${{ needs.parse.outputs.version }}" >> $GITHUB_STEP_SUMMARY | ||
echo "- **Status**: ${{ needs.publish.result }}" >> $GITHUB_STEP_SUMMARY | ||
- name: Create issue on failure | ||
if: failure() && !inputs.dry_run | ||
uses: actions/github-script@v7 | ||
with: | ||
script: | | ||
const tag = context.ref.replace('refs/tags/', ''); | ||
await github.rest.issues.create({ | ||
...context.repo, | ||
title: `Publish failed for ${tag}`, | ||
body: `Failed to publish tag \`${tag}\`\n\n[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`, | ||
labels: ['ci', 'publish-failure', 'automated'] | ||
}); |