diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index faf48f5..aecac5f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ", re.DOTALL) - - -def strip_decoration(text: str) -> str: - """Remove HTML comments and surrounding whitespace from `text`.""" - text = HTML_COMMENT_RE.sub("", text) - return text.strip() - - -def is_meaningful(text: str) -> bool: - """Return True if `text` looks like real content rather than placeholder. - - Strips HTML comments, code-fence markers and known placeholder - phrases, then checks that something other than whitespace remains. - """ - cleaned = HTML_COMMENT_RE.sub("", text) - cleaned = re.sub(r"^```\w*$|^```$", "", cleaned, flags=re.MULTILINE) - cleaned = PLACEHOLDER_RE.sub("", cleaned) - return bool(cleaned.strip()) - - -def extract_section(body: str, header: str) -> str: - """Extract the body of `##
` until the next `## ` heading. - - Returns an empty string if the section is missing or contains only - placeholder content. HTML comments inside the section are stripped - so template hint comments do not leak into release notes. - """ - if not body: - return "" - body = body.replace("\r\n", "\n").replace("\r", "\n") - pattern = re.compile( - r"^##\s+" + re.escape(header) + r"\s*\n(.*?)(?=^##\s|\Z)", - re.DOTALL | re.MULTILINE, - ) - m = pattern.search(body) - if not m: - return "" - text = m.group(1) - if not is_meaningful(text): - return "" - return strip_decoration(text) - - -# --------------------------------------------------------------------- -# Categorisation + rendering -# --------------------------------------------------------------------- - -def categorise(pr) -> str: - label_names = {l["name"].lower() for l in pr.get("labels", [])} - for title, labels in CATEGORIES: - if any(lab in label_names for lab in labels): - return title - return UNCATEGORISED - - -def excluded(pr) -> bool: - label_names = {l["name"].lower() for l in pr.get("labels", [])} - return bool(label_names & EXCLUDE_LABELS) - - -def render_pr(pr) -> str: - """Render one PR as a markdown sub-section.""" - title = pr["title"] - number = pr["number"] - author = pr.get("author") or {} - login = author.get("login", "ghost") - url = pr["url"] - body = pr.get("body") or "" - - lines = [f"### #{number} β€” {title} (@{login})", ""] - - sections_present = False - for header, display in SECTIONS: - section = extract_section(body, header) - if not section: - continue - sections_present = True - # Use a collapsible details block so long Usage code blocks - # don't blow out the release notes view. - lines.append(f"
{display}") - lines.append("") - lines.append(section) - lines.append("") - lines.append("
") - lines.append("") - - if not sections_present: - lines.append(f"_No detailed notes provided. See {url}._") - lines.append("") - - return "\n".join(lines) - - -def build_notes(repo: str, tag: str, previous: Optional[str]) -> str: - prs = [pr for pr in merged_prs_since(repo, previous) if not excluded(pr)] - - out: list[str] = [] - out.append(f"## What's Changed in {tag}") - out.append("") - - if not prs: - out.append("_No merged pull requests in this release._") - out.append("") - else: - groups: dict[str, list] = {title: [] for title, _ in CATEGORIES} - groups[UNCATEGORISED] = [] - for pr in prs: - groups[categorise(pr)].append(pr) - - for title in [c[0] for c in CATEGORIES] + [UNCATEGORISED]: - items = groups.get(title) or [] - if not items: - continue - out.append(f"## {title}") - out.append("") - for pr in items: - out.append(render_pr(pr)) - out.append("") - - if previous: - out.append( - f"**Full Changelog**: " - f"https://github.com/{repo}/compare/{previous}...{tag}" - ) - out.append("") - - return "\n".join(out).rstrip() + "\n" - - -# --------------------------------------------------------------------- -# Entrypoint -# --------------------------------------------------------------------- - -def main(): - p = argparse.ArgumentParser(description=__doc__) - p.add_argument("--tag", required=True, help="tag being published, e.g. v0.3.0") - p.add_argument("--previous", default=None, - help="previous tag (auto-detected if omitted)") - p.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY"), - help="owner/repo (defaults to GITHUB_REPOSITORY env var)") - args = p.parse_args() - - if not args.repo: - sys.exit("error: --repo or GITHUB_REPOSITORY required") - - previous = args.previous or find_previous_tag(args.tag) - print(build_notes(args.repo, args.tag, previous)) - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/AutoMerge.yml b/.github/workflows/AutoMerge.yml index c15a9c0..b5d7c0f 100644 --- a/.github/workflows/AutoMerge.yml +++ b/.github/workflows/AutoMerge.yml @@ -1,22 +1,3 @@ name: AutoMerge - -on: - pull_request: - types: [opened, synchronize, reopened, labeled] # labelεεΏœγ‚‚ε…₯γ‚Œγ‚‹γ¨δΎΏεˆ© - -permissions: - contents: write - pull-requests: write - -jobs: - auto-merge: - runs-on: ubuntu-latest - if: > - github.event.pull_request.user.type == 'Bot' || - contains(github.event.pull_request.labels.*.name, 'automerge') - steps: - - name: Enable auto-merge - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_URL: ${{ github.event.pull_request.html_url }} - run: gh pr merge --auto --merge "$PR_URL" +on: { pull_request: { types: [opened, synchronize, reopened, labeled] } } +jobs: { automerge: { uses: QAtlasHub/.github/.github/workflows/automerge.yml@main, secrets: inherit } } diff --git a/.github/workflows/AutoRegister.yml.disabled b/.github/workflows/AutoRegister.yml.disabled deleted file mode 100644 index c91a24b..0000000 --- a/.github/workflows/AutoRegister.yml.disabled +++ /dev/null @@ -1,96 +0,0 @@ -# DISABLED 2026-07-28 β€” renamed with the `.disabled` suffix, which GitHub does not load. -# -# This package is deliberately NOT in the General registry. The workflow's public path answers -# ANY Project.toml bump on main with an `@JuliaRegistrator register` comment, and the version -# here had not moved since the workflow went active, so the next ordinary PR β€” any PR, not just -# this one β€” would have published the package. Rename back to `AutoRegister.yml` when the -# package is actually meant to be registered. -# -# ---- original file below, unchanged ---- -name: AutoRegister - -# Runs after Release Drafter completes on main. -# If Project.toml version changed, posts @JuliaRegistrator register comment. - -on: - workflow_run: - workflows: ['Release Drafter'] - types: - - completed - branches: - - main - -permissions: - contents: write - issues: write - -jobs: - register: - # Only run if Release Drafter succeeded - if: github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.workflow_run.head_sha }} - fetch-depth: 2 # need HEAD and HEAD~1 to compare - - - name: Check if version changed - id: version_check - run: | - PREV=$(git show HEAD~1:Project.toml 2>/dev/null | grep '^version' | head -1 | sed 's/.*"\(.*\)"/\1/' || echo "") - CURR=$(grep '^version' Project.toml | head -1 | sed 's/.*"\(.*\)"/\1/') - echo "prev=$PREV" - echo "curr=$CURR" - echo "prev=$PREV" >> "$GITHUB_OUTPUT" - echo "curr=$CURR" >> "$GITHUB_OUTPUT" - if [ "$PREV" != "$CURR" ] && [ -n "$CURR" ]; then - echo "changed=true" >> "$GITHUB_OUTPUT" - else - echo "changed=false" >> "$GITHUB_OUTPUT" - fi - - - name: Generate release notes text - if: steps.version_check.outputs.changed == 'true' - id: notes - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CURR: ${{ steps.version_check.outputs.curr }} - run: | - DRAFT_BODY=$(gh release list --limit 50 --json tagName,isDraft \ - --jq '.[] | select(.isDraft == true) | .tagName' | head -n 1 | \ - xargs -I{} gh release view {} --json body --jq '.body' 2>/dev/null || echo "") - - if [ -z "$DRAFT_BODY" ] || [ "$DRAFT_BODY" = "null" ]; then - DRAFT_BODY="- See commit history for details in v${CURR}." - fi - - # Always wrap the body with a '## Changelog' header so the - # registered comment contains the literal 'changelog' keyword - # that Registrator's guideline checks for (required on any - # breaking release, harmless otherwise). If the upstream draft - # already carries its own header the nesting is cosmetic only. - WRAPPED=$(printf '## Changelog\n\n%s\n\nSee the full changelog at .' \ - "$DRAFT_BODY" "${{ github.repository }}" "$CURR") - delim="RELEASE_NOTES_$(openssl rand -hex 16)" - { - echo "content<<$delim" - printf '%s\n' "$WRAPPED" - echo "$delim" - } >> "$GITHUB_OUTPUT" - - - name: Post @JuliaRegistrator comment on commit - if: steps.version_check.outputs.changed == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_NOTES: ${{ steps.notes.outputs.content }} - CURR: ${{ steps.version_check.outputs.curr }} - PREV: ${{ steps.version_check.outputs.prev }} - COMMIT: ${{ github.event.workflow_run.head_sha }} - REPO: ${{ github.repository }} - run: | - BODY=$(printf '@JuliaRegistrator register\n\nRelease notes:\n\n%s\n\nVersion bumped from `%s` -> `%s` by commit %s.' \ - "$RELEASE_NOTES" "$PREV" "$CURR" "$COMMIT") - - # Registrator accepts comments on commits, not pull requests. - gh api "repos/${REPO}/commits/${COMMIT}/comments" -f body="$BODY" diff --git a/.github/workflows/CleanupPreview.yml b/.github/workflows/CleanupPreview.yml index 2b27525..64d2c1a 100644 --- a/.github/workflows/CleanupPreview.yml +++ b/.github/workflows/CleanupPreview.yml @@ -1,25 +1,3 @@ -name: CleanupPreview -on: - pull_request: - types: [closed] -permissions: - contents: write -jobs: - cleanup: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - ref: gh-pages - - name: Remove preview directory - run: | - dir="previews/PR${{ github.event.pull_request.number }}" - if [ -d "$dir" ]; then - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git rm -r "$dir" - git commit -m "chore: remove docs preview for PR#${{ github.event.pull_request.number }}" - git push - else - echo "No preview directory to remove." - fi +name: Cleanup Preview +on: { pull_request: { types: [closed] } } +jobs: { cleanup: { uses: QAtlasHub/.github/.github/workflows/docs-cleanup-preview.yml@main } } diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 963c25b..56c89b4 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,26 +1,3 @@ name: CompatHelper -on: - schedule: - - cron: '0 0 * * *' # 毎ζ—₯0時にチェック - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - CompatHelper: - runs-on: ubuntu-latest - steps: - - name: Check if Julia is already installed in the environment - if: steps.julia.outcome == 'skipped' - uses: julia-actions/setup-julia@v3 - with: - version: '1' - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - run: julia -e 'using CompatHelper; CompatHelper.main()' \ No newline at end of file +on: { schedule: [{ cron: '0 */4 * * *' }], workflow_dispatch: {} } +jobs: { compathelper: { uses: QAtlasHub/.github/.github/workflows/compathelper.yml@main, secrets: inherit } } diff --git a/.github/workflows/DocsPreview.yml b/.github/workflows/DocsPreview.yml new file mode 100644 index 0000000..e5ac40b --- /dev/null +++ b/.github/workflows/DocsPreview.yml @@ -0,0 +1,16 @@ +name: Docs Preview + +# Split out of Documentation.yml, which used to build the live site and post the PR preview +# comment in one job. The reusable pair separates them because the deploy runs on `main` and the +# preview runs on a pull request, and the two need different permissions. +# +# `preview-base` is set explicitly: the reusable defaults to `codes.sota-shimozono.com`, which +# does not resolve, whereas this repository's docs are served from GitHub Pages. The link the +# comment posts has to be one a reviewer can click. +on: { pull_request: {} } +jobs: + preview: + uses: QAtlasHub/.github/.github/workflows/docs-preview.yml@main + with: + preview-base: 'https://qatlashub.github.io' + secrets: inherit diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index eaf555b..80573a9 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -1,58 +1,3 @@ name: Documentation -on: - push: - branches: - - main - tags: '*' - pull_request: - branches: - - main - workflow_dispatch: -permissions: - contents: write - statuses: write - pull-requests: write - pages: write -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v3 - with: - version: '1' - - name: Install dependencies - run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - - name: Build and deploy - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: julia --project=docs/ docs/make.jl - - name: Trigger GitHub Pages rebuild - if: success() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - curl -s -X POST \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/${{ github.repository }}/pages/builds - - name: Find existing preview comment - if: github.event_name == 'pull_request' - id: find-comment - uses: peter-evans/find-comment@v4 - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: 'Docs preview' - - name: Post preview URL to PR - if: github.event_name == 'pull_request' && success() - uses: peter-evans/create-or-update-comment@v5 - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body: | - πŸ“š **Docs preview**: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/previews/PR${{ github.event.pull_request.number }}/ - - *(updates on each push to this PR)* - edit-mode: replace +on: { push: { branches: [main], tags: ['*'] }, workflow_dispatch: {} } +jobs: { docs: { uses: QAtlasHub/.github/.github/workflows/docs-deploy.yml@main, secrets: inherit } } diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index 1e8886d..6c151db 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -1,26 +1,8 @@ name: Format Check -on: - pull_request: - paths: ['**/*.jl'] -jobs: - format-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v3 - with: - version: '1' - - name: Install JuliaFormatter - run: julia -e 'using Pkg; Pkg.add(name="JuliaFormatter", version="2"); using JuliaFormatter; @info "JuliaFormatter $(pkgversion(JuliaFormatter))"' - - name: Check formatting - run: | - julia -e ' - using JuliaFormatter - ok = format(".", verbose=true, overwrite=false) - if !ok - println(stderr, "❌ Formatting issues found. Run JuliaFormatter locally: julia -e \"using JuliaFormatter; format(\\\".\\\")\"") - exit(1) - end - println("βœ… All files are properly formatted.") - ' +# No `paths:` filter. A filtered required check never runs on a pull request that touches no +# `.jl` file, and a required check that never runs stays pending forever β€” the pull request +# becomes unmergeable rather than exempt. This repository's `main` requires no status checks +# today, so the filter costs nothing yet; it would cost everything the day one is added. +on: { pull_request: {} } +jobs: { format: { uses: QAtlasHub/.github/.github/workflows/format-check.yml@main } } diff --git a/.github/workflows/PublishRelease.yml b/.github/workflows/PublishRelease.yml index 0cfbc6b..321229d 100644 --- a/.github/workflows/PublishRelease.yml +++ b/.github/workflows/PublishRelease.yml @@ -1,33 +1,18 @@ name: Publish Release -# Publishes a GitHub Release whenever a vX.Y.Z tag is pushed. -# -# The release notes are built by `.github/scripts/build_release_notes.py`, -# which inspects every PR merged since the previous tag, extracts the -# `## Proposed Changes` and `## Usage or Results` sections from each PR -# body, groups them by label, and writes a rich markdown report. -# -# If a draft release for this tag already exists (e.g. left by Release -# Drafter) it is overwritten with the rich notes and published. - -on: - push: - tags: - - 'v*' - workflow_dispatch: - inputs: - tag: - description: "tag to publish (e.g. v0.3.0)" - required: true +# The notes builder moved to QAtlasHub/.github/actions/release-notes, which carries the same +# `build_release_notes.py` this repository used to keep in `.github/scripts/`. +on: { push: { tags: ['v*'] }, workflow_dispatch: { inputs: { tag: { description: tag, required: true } } } } permissions: contents: write - # `build_release_notes.py` reads merged PRs through `gh pr list --search`, which resolves over - # the GraphQL search connection and needs this scope. A `permissions:` block is a REPLACEMENT, - # not an addition β€” every scope left unlisted is set to `none` β€” so `contents: write` alone left - # the job with no read access to pull requests, and every node came back as - # `Resource not accessible by integration (search.nodes.0)`. `gh` then exits 1 and the job dies - # at `Build release notes`, before `gh release edit` can ever overwrite the drafter's body. + # NOT inherited from the action, and not optional. The notes builder reads merged PRs through + # `gh pr list --search`, which resolves over the GraphQL search connection. A `permissions:` + # block REPLACES rather than adds, so `contents: write` alone leaves the job with no read + # access to pull requests and every node comes back as + # `Resource not accessible by integration (search.nodes.0)`. That failure was diagnosed and + # fixed in this repository (#33); dropping this line while adopting the shared action would + # reintroduce it. pull-requests: read jobs: @@ -35,45 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: { fetch-depth: 0 } # the builder needs history to find the previous tag + - uses: QAtlasHub/.github/actions/release-notes@v1 with: - # Need full history so the script can find the previous tag - # via `git tag --sort=-creatordate` and `git log `. - fetch-depth: 0 - - - name: Resolve tag - id: tag - run: | - if [ -n "${{ inputs.tag }}" ]; then - echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" - else - echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" - fi - - - name: Build release notes - id: notes - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p /tmp/release - python3 .github/scripts/build_release_notes.py \ - --tag "${{ steps.tag.outputs.tag }}" \ - > /tmp/release/notes.md - echo "--- generated notes ---" - cat /tmp/release/notes.md - echo "-----------------------" - - - name: Create or update release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - TAG="${{ steps.tag.outputs.tag }}" - if gh release view "$TAG" >/dev/null 2>&1; then - gh release edit "$TAG" \ - --title "$TAG" \ - --notes-file /tmp/release/notes.md \ - --draft=false - else - gh release create "$TAG" \ - --title "$TAG" \ - --notes-file /tmp/release/notes.md - fi + tag: ${{ inputs.tag || github.ref_name }} diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index ef90896..1e2592e 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,31 +1,11 @@ name: TagBot on: + schedule: + - cron: "17 4 * * *" # daily self-backfill of any registered-but-untagged version issue_comment: - types: - - created - workflow_dispatch: - inputs: - lookback: - default: "3" -permissions: - actions: read - checks: read - contents: write - deployments: read - issues: read - discussions: read - packages: read - pages: read - pull-requests: read - repository-projects: read - security-events: read - statuses: read + types: [created] # JuliaTagBot app trigger + workflow_dispatch: # manual catch-up jobs: - TagBot: - if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - runs-on: ubuntu-latest - steps: - - uses: JuliaRegistries/TagBot@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - ssh: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file + tagbot: + uses: QAtlasHub/.github/.github/workflows/tagbot.yml@main + secrets: inherit diff --git a/.github/workflows/VersionCheck.yml b/.github/workflows/VersionCheck.yml index 689fed6..6008760 100644 --- a/.github/workflows/VersionCheck.yml +++ b/.github/workflows/VersionCheck.yml @@ -1,72 +1,3 @@ name: VersionCheck - -# Ensures that every PR targeting main bumps Project.toml version. -# The check fails if the version in the PR branch is not strictly greater -# than the version on the base branch. -# -# IMPORTANT: this workflow runs on *every* PR, not only PRs that touch -# Project.toml. Restricting the trigger with `paths: Project.toml` -# would let version-bump-forgotten PRs slip through silently because -# the workflow would never run, and required-check gating treats the -# absent run as a "skipped, OK" instead of a failure. - -on: - pull_request: - branches: - - main - -permissions: - contents: read - pull-requests: write - -jobs: - check-version: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - # Fetch both the PR branch and the base branch so we can compare. - fetch-depth: 0 - - - name: Get base branch version - id: base - run: | - git fetch origin ${{ github.base_ref }} --depth=1 - VERSION=$(git show origin/${{ github.base_ref }}:Project.toml \ - | grep '^version' | head -1 | sed 's/.*"\(.*\)"/\1/') - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "Base version: $VERSION" - - - name: Get PR branch version - id: pr - run: | - VERSION=$(grep '^version' Project.toml | head -1 | sed 's/.*"\(.*\)"/\1/') - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "PR version: $VERSION" - - - name: Compare versions (semver) - env: - BASE: ${{ steps.base.outputs.version }} - PR: ${{ steps.pr.outputs.version }} - run: | - # Split a semver string into its numeric parts and compare. - version_gt() { - # Returns 0 (success) if $1 > $2 as semver, 1 otherwise. - IFS='.' read -r -a A <<< "$1" - IFS='.' read -r -a B <<< "$2" - for i in 0 1 2; do - a="${A[$i]:-0}" - b="${B[$i]:-0}" - if [ "$a" -gt "$b" ]; then return 0; fi - if [ "$a" -lt "$b" ]; then return 1; fi - done - return 1 # equal is not greater - } - - echo "Base: $BASE β†’ PR: $PR" - if version_gt "$PR" "$BASE"; then - echo "βœ“ Version bumped ($BASE β†’ $PR)" - else - echo "βœ— Version NOT bumped. PR version ($PR) must be strictly greater than base version ($BASE)." - exit 1 - fi +on: { pull_request: { branches: [main] } } +jobs: { version: { uses: QAtlasHub/.github/.github/workflows/version-check.yml@main } } diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 0e31a71..4c2dc74 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,21 +1,3 @@ name: Release Drafter - -on: - push: - branches: - - main - pull_request: - types: [opened, reopened, synchronize, labeled] - -jobs: - update_release_draft: - permissions: - contents: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@v7 - with: - commitish: main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +on: { push: { branches: [main] }, pull_request: { types: [opened, reopened, synchronize, edited] } } +jobs: { draft: { uses: QAtlasHub/.github/.github/workflows/release-drafter.yml@main, secrets: inherit } } diff --git a/README.md b/README.md index cf37dff..6c091fd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # ParallelManager.jl -[![docs: stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://codes.sota-shimozono.com/ParallelManager.jl/stable/) -[![docs: dev](https://img.shields.io/badge/docs-dev-purple.svg)](https://codes.sota-shimozono.com/ParallelManager.jl/dev/) +[![docs: dev](https://img.shields.io/badge/docs-dev-purple.svg)](https://qatlashub.github.io/ParallelManager.jl/dev/) [![Julia](https://img.shields.io/badge/julia-v1.11+-9558b2.svg)](https://julialang.org) [![Code Style: Blue](https://img.shields.io/badge/Code%20Style-Blue-4495d1.svg)](https://github.com/invenia/BlueStyle) @@ -23,9 +22,12 @@ survives kills, and logs what happened. ## Highlights - **Multi-master safe** β€” several `julia` processes can hit the same vault - root without double-executing any key (mkdir-based advisory lock). -- **Crash recovery** β€” `kill -9` a master mid-run and the next `run!` picks - up where it left off via heartbeat-based stale lock reclaim. + root without double-executing any key. Locking is delegated to + `DataVault.acquire_running!`, which uses POSIX `link()` for an atomic + "create iff not exists" that works on NFS β€” the `.running` marker is the lock. +- **Crash recovery** β€” `kill -9` a master mid-run and the next `run!` picks up + where it left off: `DataVault` writes a heartbeat into `.running`, and + `cleanup_stale` reclaims markers whose heartbeat has gone cold. - **Early skip** β€” full-done re-runs take O(1) filesystem operations (a single `manifest.jld2` read), not O(N) per-key `.done` stats. Benchmark: 3600 keys warm re-run β‰ˆ 3.5 ms. @@ -88,7 +90,6 @@ path builders that leak phase1's storage layout into phase2's code. | [`src/AtomicIO.jl`](src/AtomicIO.jl) | `atomic_write` / `atomic_touch` β€” tmp + fsync + POSIX rename, NFS-safe | | [`src/EventLog.jl`](src/EventLog.jl) | JSONL structured log, single-write atomic lines for concurrent appends | | [`src/Manifest.jl`](src/Manifest.jl) | Stage-level rollup of `canonical(key)` strings for O(1) early-skip | -| [`src/KeyLock.jl`](src/KeyLock.jl) | `mkdir` advisory lock + heartbeat + stale reclaim | | [`src/InitWorkers.jl`](src/InitWorkers.jl) | Unified `:auto` / `:sequential` / `:threads` / `:distributed` / `:slurm` bootstrap | | [`src/Run.jl`](src/Run.jl) | `run!(work_fn, vault, keys; opts)` facade that ties everything to `DataVault` | @@ -105,7 +106,7 @@ Built from direct experience with the old-style HPC loop pattern used in | `.done` files rescanned every job (3600 files, ~10 min) | `Manifest` rollup, one JLD2 read (< 10 ms) | | 300 MB of per-item `println` logs | `EventLog` (JSONL), per-item `println` is not part of the API | | Killed samples silently wedge the queue | Heartbeat + `is_stale` + `reclaim!` auto-recover on next run | -| Multiple masters double-execute the same key | `KeyLock` (`mkdir` advisory) + post-lock `is_done` re-check | +| Multiple masters double-execute the same key | `DataVault.acquire_running!` (POSIX `link()`) + post-lock `is_done` re-check | | Half-written JLD2 files after crash | `atomic_write` (tmp + fsync + rename) | | Every project reinvents SLURM / Distributed bootstrap | `init_workers!(mode=:auto)` absorbs the pattern | @@ -127,7 +128,6 @@ Requires Julia v1.11+. - `atomicio/` β€” atomic write, exception cleanup, concurrent writers - `eventlog/` β€” JSON roundtrip, 50-task Γ— 40-event concurrent write - `manifest/` β€” save/load, `todo_keys`, 3600-key bench, corrupted file -- `keylock/` β€” basic mutex, heartbeat, stale reclaim, mutual exclusion under contention - `init_workers/` β€” `:auto`, `:sequential`, `:threads`, `:slurm` env reading - `run/` β€” minimal, manifest early-skip, 4-master keylock race, retry, gave_up @@ -135,7 +135,6 @@ Requires Julia v1.11+. - [ParamIO.jl](https://github.com/QAtlasHub/ParamIO.jl) β€” config TOML parsing and `DataKey` enumeration - [DataVault.jl](https://github.com/QAtlasHub/DataVault.jl) β€” `Vault` struct, atomic JLD2 save, `.done` markers -- [templateHPC.jl](https://github.com/sotashimozono/templateHPC.jl) β€” clone-to-start scaffold that wires all three together ## License diff --git a/docs/make.jl b/docs/make.jl index 884ca8e..8d27619 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,7 +13,7 @@ Downloads.download("https://github.com/sotashimozono.png", logo_path) makedocs(; sitename="ParallelManager.jl", format=Documenter.HTML(; - canonical="https://codes.sota-shimozono.com/ParallelManager.jl/stable/", + canonical="https://qatlashub.github.io/ParallelManager.jl/dev/", prettyurls=get(ENV, "CI", "false") == "true", mathengine=MathJax3( Dict( @@ -38,7 +38,5 @@ makedocs(; ) deploydocs(; - repo="github.com/QAtlasHub/ParallelManager.jl.git", - devbranch="main", - push_preview=true, + repo="github.com/QAtlasHub/ParallelManager.jl.git", devbranch="main", push_preview=true )