Skip to content

Publish pre-release #22

Publish pre-release

Publish pre-release #22

name: Publish pre-release
on:
workflow_run:
workflows:
- CI
types:
- completed
env:
FORCE_COLOR: 1
# Serialize publishes so calendar-based .devYYYYMMDDHHMMSS versions cannot collide.
concurrency:
cancel-in-progress: false
group: ${{ github.repository }}-publish-pre-release-dev
permissions: {}
jobs:
check:
name: Check publish
if: |
github.repository == 'vacanza/holidays' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'dev'
outputs:
publish: ${{ steps.decide.outputs.publish }}
permissions:
contents: read
runs-on: ubuntu-24.04-arm
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.workflow_run.head_sha }}
# Decide from local git commit metadata.
- name: Decide whether to publish
id: decide
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { execFileSync } = require('child_process')
const commit = (fmt) =>
execFileSync('git', ['log', '-1', `--format=${fmt}`], {
encoding: 'utf8',
}).trim()
const isBot = (name, email) => {
const bots = ['dependabot[bot]', 'github-actions[bot]']
return bots.includes(name) || bots.some((bot) => email.includes(`${bot}@`))
}
const isBotCoAuthor = (message) =>
message.split('\n').some((line) => {
const match = line.match(/^Co-authored-by:\s*(.+?)(?:\s*<([^>]+)>)?\s*$/i)
return match && isBot(match[1].trim(), (match[2] || '').trim())
})
const authorEmail = commit('%ae')
const authorName = commit('%an')
const message = commit('%B')
const parents = commit('%P').split(/\s+/).filter(Boolean)
const subject = commit('%s')
let publish = true
let reason = 'publish: passed pre-release checks'
// Merge commits are mainly main to dev syncs.
if (parents.length > 1) {
publish = false
reason = `skip: merge commit (${subject})`
} else if (/^(Initialize|Finalize) v/.test(subject)) {
// Usually direct pushes to dev (no PR).
publish = false
reason = `skip: release chore commit (${subject})`
} else if (/\[skip publish\]/i.test(subject)) {
publish = false
reason = 'skip: commit subject contains [skip publish]'
} else if (isBot(authorName, authorEmail)) {
publish = false
reason = `skip: bot commit author (${authorName} <${authorEmail}>)`
} else if (isBotCoAuthor(message)) {
publish = false
reason = 'skip: bot co-author'
}
core.info(reason)
core.setOutput('publish', publish ? 'true' : 'false')
# Build/test are reusable; PyPI upload must stay top-level (Trusted Publishing).
# https://docs.pypi.org/trusted-publishers/troubleshooting/#reusable-workflows-on-github
# https://github.com/pypa/gh-action-pypi-publish/issues/166
build:
name: Build and test
needs:
- check
if: needs.check.outputs.publish == 'true'
permissions:
contents: read
uses: ./.github/workflows/run-build-tests.yml
with:
environment: pre-release
ref: ${{ github.event.workflow_run.head_sha }}
publish:
name: Publish to PyPI
needs:
- build
environment: pre-release
permissions:
id-token: write
# gh-action-pypi-publish ships an amd64-only image.
# https://github.com/pypa/gh-action-pypi-publish/issues/356
runs-on: ubuntu-24.04
steps:
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2