Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 128 additions & 68 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,93 +3,153 @@ name: "Release: Create Release"
on:
workflow_dispatch:

permissions:
contents: read

# Two runs at once would resolve the same version from the tags. Queue rather
# than cancel: a cancelled run can leave the bump pushed while the release is
# not.
concurrency:
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest

permissions:
contents: write
id-token: write
pull-requests: write

env:
BRANCH: ci/bump-version
timeout-minutes: 15

steps:
- name: Refuse to release from anything but the default branch
if: ${{ github.ref_name != github.event.repository.default_branch }}
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
REF_NAME: ${{ github.ref_name }}
shell: bash
run: |
echo "::error::Releases are cut from ${DEFAULT_BRANCH}, not ${REF_NAME}."
exit 1

# The App only, with no fallback token: the default token cannot pass the
# pull request rule of the default branch, and a release it creates
# triggers neither the image build nor the documentation deploy.
- name: Setup Git User
uses: mcanouil/quarto-workflows/.github/actions/setup-git-user@v2
id: setup-git-user
with:
gh-app-id: ${{ vars.APP_ID }}
app-key: ${{ secrets.APP_KEY }}

- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: true
fetch-tags: true
token: ${{ steps.setup-git-user.outputs.token }}
# The whole history and every tag, since the same-day suffix is
# resolved from the tags already cut.
fetch-depth: 0

- name: Create GitHub App token
uses: actions/create-github-app-token@v3
id: app-token
# The first release of a day is the bare date; every further one carries
# the count of releases cut that day, such as `2026.10.16-2`. An empty
# Unreleased section in CHANGELOG.md stops the release here.
- name: Bump the version
id: bump
uses: mcanouil/release-actions/bump@v1
with:
client-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_KEY }}

- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
bump: calver
version-source: tag
targets: |
- file: CITATION.cff
pattern: '^version: {version}$'
- file: CITATION.cff
pattern: '^date-released: "{date}"$'

# The release step refuses an existing tag or release too, but only after
# the push, which would leave the bump on the default branch.
- name: Refuse an existing tag or release
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

- name: Git Config
GH_TOKEN: ${{ steps.setup-git-user.outputs.token }}
VERSION: ${{ steps.bump.outputs.version }}
shell: bash
run: |
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
set -euo pipefail
STATUS=0
git ls-remote --exit-code --tags origin "refs/tags/${VERSION}" > /dev/null || STATUS=$?
case "${STATUS}" in
0) echo "::error::Tag ${VERSION} already exists. Delete it, or create its release by hand, then dispatch this workflow again."; exit 1 ;;
2) ;;
*) echo "::error::Could not list the tags of origin (git exit ${STATUS})."; exit 1 ;;
esac
if OUTPUT=$(gh release view "${VERSION}" 2>&1); then
echo "::error::A release ${VERSION} already exists, perhaps as a draft. Delete it, then dispatch this workflow again."
exit 1
fi
if ! grep -qi 'release not found' <<< "${OUTPUT}"; then
echo "::error::Could not check for release ${VERSION}: ${OUTPUT}"
exit 1
fi

- name: Bump Version
id: bump-version
# The App is exempt from the pull request rule of the default branch, and
# [skip ci] keeps CI from running again on a tree it has checked.
- name: Push the version
id: push
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
COMMIT: "ci: bump version for release :rocket:"
VERSION: ${{ steps.bump.outputs.version }}
FILES: ${{ steps.bump.outputs.files }}
shell: bash
run: |
if git show-ref --quiet refs/heads/${BRANCH}; then
echo "Branch ${BRANCH} already exists."
git branch -D "${BRANCH}"
git push origin --delete "${BRANCH}"
set -euo pipefail
printf '%s\n' "${FILES}" | git add --pathspec-from-file=-
git commit -m "ci: release ${VERSION} [skip ci]"
if ! git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}"; then
echo "::error::The App could not push the bump to ${GITHUB_REF_NAME}. If the push output says the branch moved, dispatch this workflow again; otherwise add the App to the bypass list of the branch ruleset first."
exit 1
fi
echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"

echo "$(git tag -l --sort=-v:refname | head -n 5)"

git checkout -b "${BRANCH}"

DATE=$(date +%Y-%m-%d)
BASE_VERSION=$(date +%Y.%m.%d)

SUFFIX=1
VERSION="${BASE_VERSION}"
while [ -n "$(git tag -l "${VERSION}")" ]; do
echo "Tag ${VERSION} already exists, incrementing suffix..."
SUFFIX=$((SUFFIX + 1))
VERSION="${BASE_VERSION}-${SUFFIX}"
done
echo "New version will be: ${VERSION}"
sed -i "s/^version:.*/version: ${VERSION}/" CITATION.cff
sed -i "s/^date-released:.*/date-released: \"${DATE}\"/" CITATION.cff

git add CITATION.cff || echo "No changes to add"
git commit -m "${COMMIT}" || echo "No changes to commit"
git push --force origin ${BRANCH} || echo "No changes to push"

sleep 5
gh pr create --fill-first --base "main" --head "${BRANCH}" --label "Type: CI/CD :robot:"
sleep 2
gh pr merge --auto --squash --delete-branch

- name: Update branch
- name: Compose the release body
id: notes
uses: mcanouil/release-actions/notes@v1
with:
notes-file: ${{ steps.bump.outputs.notes-file }}
tag: ${{ steps.bump.outputs.version }}
target: ${{ steps.push.outputs.sha }}
generate-notes: true
token: ${{ steps.setup-git-user.outputs.token }}

# A further release of a day is titled with its count, such as
# `2026-10-16 (2)`.
- name: Resolve the release title
id: title
env:
VERSION: ${{ steps.bump.outputs.version }}
DATE: ${{ steps.bump.outputs.date }}
shell: bash
run: |
git fetch --all
git checkout main
git pull origin main
set -euo pipefail
TITLE="${DATE}"
if [[ "${VERSION}" == *-* ]]; then
TITLE="${DATE} (${VERSION##*-})"
fi
echo "title=${TITLE}" >> "${GITHUB_OUTPUT}"

- name: Release extension on GitHub
# Created as the App, so that the published release starts the image
# build and the documentation deploy.
- name: Release on GitHub
uses: mcanouil/release-actions/release@v1
with:
version: ${{ steps.bump.outputs.version }}
body-file: ${{ steps.notes.outputs.body-file }}
title: ${{ steps.title.outputs.title }}
target: ${{ steps.push.outputs.sha }}
latest: true
token: ${{ steps.setup-git-user.outputs.token }}

# A new dispatch would stop on the emptied Unreleased section, so the
# pushed bump has to go first.
- name: Explain the recovery
if: ${{ failure() && steps.push.outputs.sha != '' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ steps.bump.outputs.version }}
SHA: ${{ steps.push.outputs.sha }}
shell: bash
run: |
DATE=$(grep "^date-released:" CITATION.cff | sed 's/date-released: //;s/"//g')
VERSION=$(grep "^version:" CITATION.cff | sed 's/version: //;s/"//g')
gh release create ${VERSION} --title ${DATE} --generate-notes
echo "::error::${GITHUB_REF_NAME} holds the bump ${SHA} for ${VERSION}, but the release failed. Create release ${VERSION} on ${SHA} by hand, or revert that commit with a pull request and dispatch this workflow again."
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## Unreleased

- feat: Release notes now come from this changelog. (#182)
- feat: The documentation website has icons and a social preview card. (#165)
- fix: Screen readers no longer read the navigation bar and footer icons twice.
4 changes: 2 additions & 2 deletions docs/_extensions/mcanouil/atelier/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
title: Atelier
author: Mickaël Canouil
version: 0.10.5
version: 0.10.7
quarto-required: ">=1.9.36"
contributes:
project:
Expand Down Expand Up @@ -84,5 +84,5 @@ contributes:
- file: html/scripts/ordinal-dates.html
- file: html/scripts/a11y-fixes.html
- file: html/scripts/navbar-tooltips.html
source: mcanouil/quarto-atelier@0.10.5
source: mcanouil/quarto-atelier@0.10.7
source-type: github
62 changes: 51 additions & 11 deletions docs/_extensions/mcanouil/atelier/html/chrome.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*-- scss:functions --*/

// Mix $color towards black or white, as Quarto's `theme-contrast()` does, until
// it reads at 4.5:1 on its own soft tint: $color at $alpha over $bg. The tint
// darkens with the colour, so it is recomputed at every step.
@function atelier-contrast-on-tint($color, $bg, $alpha) {
$toward: if(tone($bg) == "light", #000, #fff);
@for $step from 0 through 100 {
$candidate: mix($toward, $color, $step * 1%);
@if quarto-contrast($candidate, mix($candidate, $bg, $alpha * 100%)) >= 4.5 {
@return $candidate;
}
}
@return $toward;
}

/*-- scss:defaults --*/

// ===========================================================================
Expand Down Expand Up @@ -58,11 +74,10 @@ $atelier-chrome-bg: if(
)
) !default;

// Both inks are run through Quarto's own `theme-contrast()`, which mixes a
// colour towards black or white only until it clears the ratio asked of it and
// returns it untouched otherwise. In `auto` that matters for the accent: a
// brand primary is picked against the page, and the bar is a tint of the page,
// so a primary sitting just above 4.5:1 on the page can fall under it here.
// Both inks are mixed towards black or white only until they clear 4.5:1, and
// returned untouched when they already do; a ratio that cannot be met ends at
// black or white. In `auto` the bar is a tint of the page, so a brand colour
// sitting just above 4.5:1 on the page can fall under it here.
// Setting either variable in a project skips the correction, since a `!default`
// assignment is not evaluated at all once the variable is set.
$atelier-chrome-fg: theme-contrast(
Expand All @@ -79,7 +94,14 @@ $atelier-chrome-fg: theme-contrast(
"AA"
) !default;

$atelier-chrome-accent: theme-contrast(
// The surface of dropdown menus, a step from the bar towards the ink.
$atelier-chrome-surface: mix($atelier-chrome-bg, $atelier-chrome-fg, 90%) !default;

// The accent sits on the bar, and on the accent-soft pill of the active sidebar
// item and of a hovered dropdown item. The pill over the surface is the closest
// of these to the accent, so holding the accent to AA there covers the rest.
$atelier-chrome-accent-soft-alpha: 0.16 !default;
$atelier-chrome-accent: atelier-contrast-on-tint(
if(
$atelier-chrome == dark,
$atelier-chrome-dark-accent,
Expand All @@ -89,15 +111,33 @@ $atelier-chrome-accent: theme-contrast(
$atelier-chrome-page-accent
)
),
$atelier-chrome-bg,
"AA"
$atelier-chrome-surface,
$atelier-chrome-accent-soft-alpha
) !default;

// Everything else follows from that triple, so a project re-points three
// colours rather than eight.
$atelier-chrome-surface: mix($atelier-chrome-bg, $atelier-chrome-fg, 90%) !default;
$atelier-chrome-muted: mix($atelier-chrome-fg, $atelier-chrome-bg, 65%) !default;
$atelier-chrome-accent-soft: rgba($atelier-chrome-accent, 0.16) !default;
$atelier-chrome-muted: theme-contrast(
mix($atelier-chrome-fg, $atelier-chrome-bg, 65%),
$atelier-chrome-bg,
"AA"
) !default;
$atelier-chrome-accent-soft: rgba($atelier-chrome-accent, $atelier-chrome-accent-soft-alpha) !default;

// ===========================================================================
// PAGE VARIABLES
// ===========================================================================
// The website listing layer pins $text-muted to a fixed #6c757d ahead of this
// file, which is under 4.5:1 on most pages; derive it from the page instead,
// held to AA. A `!default` would lose to that layer, so it is handed over by
// plain assignment, like the chrome variables below; a project sets
// $atelier-text-muted instead.
$atelier-text-muted: theme-contrast(
mix($atelier-chrome-page-fg, $atelier-chrome-page-bg, 65%),
$atelier-chrome-page-bg,
"AA"
) !default;
$text-muted: $atelier-text-muted;

// A hairline between two surfaces, which is what the bar edges and the section
// dividers want.
Expand Down
Loading
Loading