From b07e0ccd106b0f2a4315554b7dfba65ea7723604 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:25 -0400 Subject: [PATCH 01/20] ci: add revival generator workflow --- .github/workflows/revival-start.yml | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/revival-start.yml diff --git a/.github/workflows/revival-start.yml b/.github/workflows/revival-start.yml new file mode 100644 index 0000000..93fa2b1 --- /dev/null +++ b/.github/workflows/revival-start.yml @@ -0,0 +1,38 @@ +name: Start a module revival + +on: + workflow_dispatch: + inputs: + module: + description: "Module name (e.g. BuildHelpers). Used in titles and, by default, as the repo name." + required: true + steward: + description: "GitHub login of the Steward (assigned to every issue)" + required: false + repo: + description: "Repo to pull baseline metrics from, if not PowerShellOrg/" + required: false + adoption_issue: + description: "Adoption request issue number to comment on, if any" + required: false + +permissions: + issues: write + contents: read + +concurrency: + group: revival-${{ inputs.module }} + cancel-in-progress: false + +jobs: + create: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/github-script@v8 + env: + INPUTS: ${{ toJSON(inputs) }} + with: + script: | + const run = require('./scripts/create-revival.js'); + await run({ github, context, core, inputs: JSON.parse(process.env.INPUTS) }); From 93d89a4abb30bb94a2e56e17dc00041e72a08e04 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:27 -0400 Subject: [PATCH 02/20] feat: revival generator script --- scripts/create-revival.js | 118 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 scripts/create-revival.js diff --git a/scripts/create-revival.js b/scripts/create-revival.js new file mode 100644 index 0000000..b6791f2 --- /dev/null +++ b/scripts/create-revival.js @@ -0,0 +1,118 @@ +// Opens one [REVIVAL] tracking issue plus six phase sub-issues from docs/revival/*.md. +// Idempotent: re-running for the same module finds the existing tracking issue and stops. +// Runs under the default GITHUB_TOKEN (issues: write). Needs nothing else. +const fs = require('fs'); +const path = require('path'); + +const SOURCE_DIR = 'docs/revival'; +const PHASES = ['phase-0', 'phase-1', 'phase-2', 'phase-3', 'phase-4', 'phase-5']; +const TRACKING_LABEL = 'revival'; + +function parseSource(file) { + const raw = fs.readFileSync(path.join(process.cwd(), SOURCE_DIR, `${file}.md`), 'utf8'); + const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); + if (!m) throw new Error(`${file}.md has no frontmatter`); + const meta = {}; + for (const line of m[1].split(/\r?\n/)) { + const kv = line.match(/^(\w+):\s*(.*)$/); + if (kv) meta[kv[1]] = kv[2].replace(/^"(.*)"$/, '$1').trim(); + } + const labels = (meta.labels || '').split(',').map(s => s.trim()).filter(Boolean); + return { title: meta.title, type: meta.type || null, labels, body: raw.slice(m[0].length) }; +} + +function fill(text, vars) { + return text.replace(/\{\{(\w+)\}\}/g, (_, k) => (vars[k] ?? `_${k}_`)); +} + +async function baseline(github, fullName) { + const blank = { last_push: '_unknown_', open_issues: '_unknown_', open_prs: '_unknown_', stars: '_unknown_', forks: '_unknown_', ci: '_unknown_' }; + const [owner, repo] = fullName.split('/'); + let r; + try { r = (await github.rest.repos.get({ owner, repo })).data; } + catch (e) { if (e.status === 404) return { ...blank, repo_url: `https://github.com/${fullName}`, note: 'repo not found; fill baseline by hand' }; throw e; } + // open_issues_count includes PRs; subtract an exact PR count. No search API: it lags and misfired under GITHUB_TOKEN. + const open_prs = (await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 })).length; + const open_issues = r.open_issues_count - open_prs; + const ci = []; + for (const [p, name] of [['.github/workflows', 'GitHub Actions'], ['appveyor.yml', 'AppVeyor'], ['azure-pipelines.yml', 'Azure Pipelines'], ['.travis.yml', 'Travis']]) { + try { await github.rest.repos.getContent({ owner, repo, path: p }); ci.push(name); } catch (e) { if (e.status !== 404) throw e; } + } + return { + repo_url: r.html_url, last_push: r.pushed_at.slice(0, 10), open_issues, open_prs, + stars: r.stargazers_count, forks: r.forks_count, ci: ci.length ? ci.join(', ') : 'none', + }; +} + +async function findExisting(github, owner, repo, title) { + const issues = await github.paginate(github.rest.issues.listForRepo, { owner, repo, labels: TRACKING_LABEL, state: 'all', per_page: 100 }); + return issues.find(i => !i.pull_request && i.title === title) || null; +} + +async function ensureLabel(github, owner, repo) { + try { await github.rest.issues.getLabel({ owner, repo, name: TRACKING_LABEL }); } + catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner, repo, name: TRACKING_LABEL, color: '5319e7', description: 'Module revival tracking' }); + } +} + +async function createIssue(github, owner, repo, src, vars) { + const params = { owner, repo, title: fill(src.title, vars), body: fill(src.body, vars), labels: src.labels }; + if (src.type) params.type = src.type; + if (vars.steward !== 'unassigned') params.assignees = [vars.steward.slice(1)]; + try { + return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; + } catch (e) { + // Issue type unknown to this org, or steward not assignable: retry bare rather than fail the run. + if (e.status !== 422) throw e; + console.warn(`422 creating "${params.title}" (${e.message}); retrying without type/assignees`); + delete params.type; delete params.assignees; + return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; + } +} + +module.exports = async function run({ github, context, core, inputs }) { + const { owner, repo } = context.repo; + const module = inputs.module.trim(); + const target = (inputs.repo || `${owner}/${module}`).trim(); + const tracking = parseSource('tracking'); + + const vars = { + module, steward: inputs.steward ? '@' + inputs.steward.trim().replace(/^@/, '') : 'unassigned', + today: new Date().toISOString().slice(0, 10), ...(await baseline(github, target)), + }; + const title = fill(tracking.title, vars); + + const existing = await findExisting(github, owner, repo, title); + if (existing) { + core.summary.addRaw(`Tracking issue already exists: [#${existing.number}](${existing.html_url}) (${existing.state}). Nothing created.`).write(); + core.setOutput('tracking_issue', existing.number); + return; + } + + await ensureLabel(github, owner, repo); + const parent = await createIssue(github, owner, repo, tracking, vars); + core.info(`Created ${parent.html_url}`); + + const children = []; + for (const name of PHASES) { + const child = await createIssue(github, owner, repo, parseSource(name), vars); + // sub_issue_id is the database id, not the issue number. + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner, repo, issue_number: parent.number, sub_issue_id: child.id }); + children.push(child); + core.info(` + ${child.title} -> #${child.number}`); + } + + if (inputs.adoption_issue) { + await github.rest.issues.createComment({ owner, repo, issue_number: Number(inputs.adoption_issue), + body: `Revival tracking issue opened: #${parent.number}. Steward: ${vars.steward}.` }); + } + + core.setOutput('tracking_issue', parent.number); + core.summary.addHeading(`Revival started: ${module}`) + .addRaw(`Tracking issue [#${parent.number}](${parent.html_url}) with ${children.length} phase sub-issues.`) + .addList(children.map(c => `#${c.number} ${c.title}`)) + .addRaw(vars.note ? `\n\n> ${vars.note}` : '') + .write(); +}; From 3371df5eaf9d6d2670b3e6dbc76de36c7cc414a9 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:28 -0400 Subject: [PATCH 03/20] docs: revival label manifest --- docs/revival/labels.yml | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/revival/labels.yml diff --git a/docs/revival/labels.yml b/docs/revival/labels.yml new file mode 100644 index 0000000..32d5eb4 --- /dev/null +++ b/docs/revival/labels.yml @@ -0,0 +1,74 @@ +# PowerShellOrg module-revival label manifest +# Destination: PowerShellOrg/.github (pending write access — see wayfinder T1) +# +# Convention: colon-space, lowercase. No emoji. No "/", "&", or apostrophes in names — +# they break label-sync tooling (vscode-powershell carries both "Area-Build & Release" +# and "Area-Build Release" from exactly this). +# +# One colour per facet. Only priority gets a gradient, and it lives in an Issue Field, +# not here. +# +# NOT in this file, deliberately: +# type: -> native org Issue Types (Task / Bug / Feature) — already enabled +# priority: -> org Issue Field "Priority" — Urgent / High / Medium / Low +# effort: -> dropped. Every human-estimated effort facet measured is dead or dying +# skill: -> dropped. Zero of 15 large repos surveyed track it + +# --------------------------------------------------------------------------- +# Discovery — EXACT strings GitHub indexes. Do not prefix, TitleCase, or emoji these. +# "good first issue" populates the repo Contribute page and feeds GitHub's +# approachability algorithm. Getting these wrong makes a revival invisible to the +# contributor funnel it exists to attract. +# --------------------------------------------------------------------------- +- name: "good first issue" + color: "7057ff" + description: "Newcomer-ready: solution explained, code identified, ready to test" +- name: "help wanted" + color: "008672" + description: "Maintainer welcomes a PR on this" + +# --------------------------------------------------------------------------- +# area: — what part of the project. Multi-valued. The four below are universal; +# the Steward adds module-specific ones (see the recipe in the playbook). +# --------------------------------------------------------------------------- +- name: "area: build" + color: "0052cc" + description: "CI, psake, packaging, release pipeline" +- name: "area: tests" + color: "0052cc" + description: "Pester tests and test infrastructure" +- name: "area: docs" + color: "0052cc" + description: "README, help text, examples, CONTRIBUTING" +- name: "area: triage" + color: "0052cc" + description: "Issue and PR triage, backlog cleanup" + +# --------------------------------------------------------------------------- +# status: — transient lifecycle state, often written by bots. Not a taxonomy. +# --------------------------------------------------------------------------- +- name: "status: needs-repro" + color: "bfc7d1" + description: "Awaiting reproduction steps or clarification from the reporter" +- name: "status: stale" + color: "bfc7d1" + description: "No recent activity; candidate for closing" + +# --------------------------------------------------------------------------- +# PRUNE — delete these on transfer. Each is superseded, not merely unfashionable. +# bug, enhancement -> native Issue Types (Bug, Feature) +# documentation -> area: docs +# duplicate -> native close reason "Duplicate" +# wontfix, invalid -> native close reason "Not planned" +# question -> GitHub Discussions (the playbook already routes these there) +# Prune only after checking usage; a label with real history may be worth migrating +# rather than deleting. +# --------------------------------------------------------------------------- +prune: + - "bug" + - "enhancement" + - "documentation" + - "duplicate" + - "wontfix" + - "invalid" + - "question" From 8900f09e3ed9d968e3be69dc287efe9e1459b35e Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:30 -0400 Subject: [PATCH 04/20] docs: revival source tracking --- docs/revival/tracking.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/revival/tracking.md diff --git a/docs/revival/tracking.md b/docs/revival/tracking.md new file mode 100644 index 0000000..0e58bd6 --- /dev/null +++ b/docs/revival/tracking.md @@ -0,0 +1,34 @@ +--- +title: "[REVIVAL] {{module}}" +labels: revival +type: Task +--- + +# {{module}} revival + +Tracking issue for bringing **{{module}}** from `status-incoming` to `status-active`. +Steward: {{steward}}. Repo: {{repo_url}}. + +Each phase is a sub-issue below. Phases overlap; pace to the state of the repo, not the +week numbers. Reference material (comment templates, decision tree, YAML, release steps) is in +the [Revival Playbook](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md). + +## Baseline (captured at transfer) + +| Metric | Value | +|---|---| +| Transfer date | {{today}} | +| Last commit | {{last_push}} | +| Open issues | {{open_issues}} | +| Open PRs | {{open_prs}} | +| Stars / forks | {{stars}} / {{forks}} | +| Existing CI | {{ci}} | +| Last PSGallery release | _fill in: version and date_ | +| PSGallery downloads | _fill in_ | + +These numbers go in the first release announcement. + +## Working with an AI assistant + +Point it at the playbook and this issue. It drafts; you post. Comments on other people's +issues, PR closes, and merges are yours to click. From 4f95f419a4e16c5ddb76efd2aa645616d9e11343 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:31 -0400 Subject: [PATCH 05/20] docs: revival source phase-0 --- docs/revival/phase-0.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/revival/phase-0.md diff --git a/docs/revival/phase-0.md b/docs/revival/phase-0.md new file mode 100644 index 0000000..ad25ae3 --- /dev/null +++ b/docs/revival/phase-0.md @@ -0,0 +1,26 @@ +--- +title: "[REVIVAL] {{module}} - Phase 0: Inventory" +labels: revival +type: Task +role: Steward (PSGallery items: Org Admin) +--- + +Reference: [Playbook - Phase 0](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-0-take-inventory) + +## Transfer and access + +- [ ] Repo lives at `github.com/PowerShellOrg/{{module}}` +- [ ] Repo marked `status-incoming` +- [ ] Steward and maintainers have Write +- [ ] Branch protection on the default branch +- [ ] Default branch is `main` +- [ ] Labels match [`labels.yml`](https://github.com/PowerShellOrg/.github/blob/main/docs/revival/labels.yml) + +## PSGallery (Org Admin) + +- [ ] Package ownership transferred to the PowerShellOrg PSGallery account +- [ ] Scoped API key created: `PowerShellOrg-{{module}}-`, 365-day expiry +- [ ] `PSGALLERY_API_KEY` secret set on the repo +- [ ] Rotation reminder on the Org Admin's tracking issue + +Baseline metrics are in the tracking issue; fill in the PSGallery rows there. From 9a963d5794b95184301980eddcc0aa647ab393ff Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:32 -0400 Subject: [PATCH 06/20] docs: revival source phase-1 --- docs/revival/phase-1.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/revival/phase-1.md diff --git a/docs/revival/phase-1.md b/docs/revival/phase-1.md new file mode 100644 index 0000000..fd0bc86 --- /dev/null +++ b/docs/revival/phase-1.md @@ -0,0 +1,16 @@ +--- +title: "[REVIVAL] {{module}} - Phase 1: Issue triage" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 1](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-1-issue-triage) +(per-issue procedure and comment templates) + +Goal: every open issue is typed, acknowledged, and either closed or has a clear next step. +An issue with no Issue Type is untriaged. + +- [ ] Every open issue has an Issue Type +- [ ] Every open issue is either closed, waiting on the reporter (`status: needs-repro`), or has a next step +- [ ] Pre-adoption issues with no activity in 2 years are closed with the [stale comment](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#closing-a-stale-pre-adoption-issue) From f1b448801447d3e89074e18b9d0aef2a829461d9 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:34 -0400 Subject: [PATCH 07/20] docs: revival source phase-2 --- docs/revival/phase-2.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docs/revival/phase-2.md diff --git a/docs/revival/phase-2.md b/docs/revival/phase-2.md new file mode 100644 index 0000000..28ed8ba --- /dev/null +++ b/docs/revival/phase-2.md @@ -0,0 +1,14 @@ +--- +title: "[REVIVAL] {{module}} - Phase 2: PR triage" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 2](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-2-pr-triage) +(decision tree, take-over and close templates) + +Goal: no PR sits without a decision. Runs alongside Phase 1. + +- [ ] No open PR is older than 30 days without a maintainer comment +- [ ] Every open PR is merged, closed with a note, or actively under review From 9094f1ab887c0db395a924366ed668ce38c53138 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:35 -0400 Subject: [PATCH 08/20] docs: revival source phase-3 --- docs/revival/phase-3.md | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/revival/phase-3.md diff --git a/docs/revival/phase-3.md b/docs/revival/phase-3.md new file mode 100644 index 0000000..2fdd4fe --- /dev/null +++ b/docs/revival/phase-3.md @@ -0,0 +1,51 @@ +--- +title: "[REVIVAL] {{module}} - Phase 3: Build modernization" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 3](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-3-build-modernization) +(standard stack, CI and release YAML) + +Goal: the repo builds with the standard stack and CI is green on all platforms. +Work in order; each step is its own PR. + +## 3a Pester + +- [ ] Tests exist and use Pester 5 +- [ ] `Invoke-Pester` passes locally + +## 3b psake + +- [ ] `psakeFile.ps1` defines `Init`, `Clean`, `Build`, `Test`, `Analyze`, `Publish` +- [ ] `Invoke-psake ?` lists them +- [ ] `Invoke-psake Test` passes + +## 3c PowerShellBuild + +- [ ] Build references PowerShellBuild for shared task logic +- [ ] `Test-ModuleManifest` passes +- [ ] `Invoke-psake Build` stages a clean module in `output/` + +## 3d PSScriptAnalyzer + +- [ ] `Invoke-psake Analyze` runs the org ruleset +- [ ] Zero warnings, or each suppression carries a justifying comment + +## 3e CI + +- [ ] `.github/workflows/ci.yml` calls the org reusable workflow +- [ ] CI green on `main` across Win/PS5.1, Win/PS7, Linux/PS7, macOS/PS7 +- [ ] Branch protection requires the CI check + +## 3f Release workflow + +- [ ] `.github/workflows/release.yml` calls the org reusable workflow +- [ ] A pre-release tag (`v*-beta.1`) ran the workflow end to end + +## 3g Coverage + +- [ ] Coverage report generated +- [ ] Baseline percentage recorded in the tracking issue +- [ ] Critical public functions have tests; gaps filed as issues From dbf8dfb79433268ce500542a5d288076889c67d8 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:36 -0400 Subject: [PATCH 09/20] docs: revival source phase-4 --- docs/revival/phase-4.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/revival/phase-4.md diff --git a/docs/revival/phase-4.md b/docs/revival/phase-4.md new file mode 100644 index 0000000..28f5ccc --- /dev/null +++ b/docs/revival/phase-4.md @@ -0,0 +1,27 @@ +--- +title: "[REVIVAL] {{module}} - Phase 4: First clean release" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 4](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-4-first-clean-release) +(release steps) + +Goal: a release under the PowerShellOrg banner you are proud to put your name on. + +## Release gate + +- [ ] `CHANGELOG.md` covers everything since the last release +- [ ] Version bumped: patch / minor / major as the changes warrant +- [ ] CI green and PSScriptAnalyzer clean +- [ ] Manifest accurate: description, author or org, copyright, tags, URLs +- [ ] `README.md` current: install, working examples, badges +- [ ] Open critical bugs fixed or deferred with a written reason +- [ ] Release PR reviewed by one other maintainer (or Steward if solo) + +## Exit + +- [ ] Release on PSGallery under PowerShellOrg +- [ ] GitHub Release with human-readable notes +- [ ] PSGallery package description says PowerShellOrg From 259c62c2c1f5da0c3fc6dc7d52a9d62af99381e6 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:01:37 -0400 Subject: [PATCH 10/20] docs: revival source phase-5 --- docs/revival/phase-5.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/revival/phase-5.md diff --git a/docs/revival/phase-5.md b/docs/revival/phase-5.md new file mode 100644 index 0000000..6e37b5c --- /dev/null +++ b/docs/revival/phase-5.md @@ -0,0 +1,16 @@ +--- +title: "[REVIVAL] {{module}} - Phase 5: Transition to active" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 5](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-5-ongoing-maintenance) +(cadence table, graduation criteria) + +Closing this closes the revival. Ongoing cadence and graduation are in the playbook. + +- [ ] Repo marked `status-active` (was `status-incoming`) +- [ ] `README.md` status badge updated +- [ ] Org Admin notified +- [ ] Announced in the Council channel From 03540c8ab30b9a05706640845491d1ca7bff11f8 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:10:37 -0400 Subject: [PATCH 11/20] docs: point phase-0 reference links at existing playbook anchors --- docs/revival/phase-0.md | 52 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/revival/phase-0.md b/docs/revival/phase-0.md index ad25ae3..dd43ab3 100644 --- a/docs/revival/phase-0.md +++ b/docs/revival/phase-0.md @@ -1,26 +1,26 @@ ---- -title: "[REVIVAL] {{module}} - Phase 0: Inventory" -labels: revival -type: Task -role: Steward (PSGallery items: Org Admin) ---- - -Reference: [Playbook - Phase 0](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-0-take-inventory) - -## Transfer and access - -- [ ] Repo lives at `github.com/PowerShellOrg/{{module}}` -- [ ] Repo marked `status-incoming` -- [ ] Steward and maintainers have Write -- [ ] Branch protection on the default branch -- [ ] Default branch is `main` -- [ ] Labels match [`labels.yml`](https://github.com/PowerShellOrg/.github/blob/main/docs/revival/labels.yml) - -## PSGallery (Org Admin) - -- [ ] Package ownership transferred to the PowerShellOrg PSGallery account -- [ ] Scoped API key created: `PowerShellOrg-{{module}}-`, 365-day expiry -- [ ] `PSGALLERY_API_KEY` secret set on the repo -- [ ] Rotation reminder on the Org Admin's tracking issue - -Baseline metrics are in the tracking issue; fill in the PSGallery rows there. +--- +title: "[REVIVAL] {{module}} - Phase 0: Inventory" +labels: revival +type: Task +role: Steward (PSGallery items: Org Admin) +--- + +Reference: [Playbook - Phase 0](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-0-take-inventory-week-1) + +## Transfer and access + +- [ ] Repo lives at `github.com/PowerShellOrg/{{module}}` +- [ ] Repo marked `status-incoming` +- [ ] Steward and maintainers have Write +- [ ] Branch protection on the default branch +- [ ] Default branch is `main` +- [ ] Labels match [`labels.yml`](https://github.com/PowerShellOrg/.github/blob/main/docs/revival/labels.yml) + +## PSGallery (Org Admin) + +- [ ] Package ownership transferred to the PowerShellOrg PSGallery account +- [ ] Scoped API key created: `PowerShellOrg-{{module}}-`, 365-day expiry +- [ ] `PSGALLERY_API_KEY` secret set on the repo +- [ ] Rotation reminder on the Org Admin's tracking issue + +Baseline metrics are in the tracking issue; fill in the PSGallery rows there. From 03705dc973d9b759b546e785696eec0701a2ff9d Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:10:38 -0400 Subject: [PATCH 12/20] docs: point phase-1 reference links at existing playbook anchors --- docs/revival/phase-1.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/revival/phase-1.md b/docs/revival/phase-1.md index fd0bc86..2cae965 100644 --- a/docs/revival/phase-1.md +++ b/docs/revival/phase-1.md @@ -1,16 +1,16 @@ ---- -title: "[REVIVAL] {{module}} - Phase 1: Issue triage" -labels: revival -type: Task -role: Steward ---- - -Reference: [Playbook - Phase 1](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-1-issue-triage) -(per-issue procedure and comment templates) - -Goal: every open issue is typed, acknowledged, and either closed or has a clear next step. -An issue with no Issue Type is untriaged. - -- [ ] Every open issue has an Issue Type -- [ ] Every open issue is either closed, waiting on the reporter (`status: needs-repro`), or has a next step -- [ ] Pre-adoption issues with no activity in 2 years are closed with the [stale comment](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#closing-a-stale-pre-adoption-issue) +--- +title: "[REVIVAL] {{module}} - Phase 1: Issue triage" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 1](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-1-issue-triage-weeks-1-4) +(per-issue procedure and comment templates) + +Goal: every open issue is typed, acknowledged, and either closed or has a clear next step. +An issue with no Issue Type is untriaged. + +- [ ] Every open issue has an Issue Type +- [ ] Every open issue is either closed, waiting on the reporter (`status: needs-repro`), or has a next step +- [ ] Pre-adoption issues with no activity in 2 years are closed with the [stale comment](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#comment-templates) From 11ce25a3ee763b28b727ef8ea62b14832ecc3f4b Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:10:40 -0400 Subject: [PATCH 13/20] docs: point phase-2 reference links at existing playbook anchors --- docs/revival/phase-2.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/revival/phase-2.md b/docs/revival/phase-2.md index 28ed8ba..4453bb2 100644 --- a/docs/revival/phase-2.md +++ b/docs/revival/phase-2.md @@ -1,14 +1,14 @@ ---- -title: "[REVIVAL] {{module}} - Phase 2: PR triage" -labels: revival -type: Task -role: Steward ---- - -Reference: [Playbook - Phase 2](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-2-pr-triage) -(decision tree, take-over and close templates) - -Goal: no PR sits without a decision. Runs alongside Phase 1. - -- [ ] No open PR is older than 30 days without a maintainer comment -- [ ] Every open PR is merged, closed with a note, or actively under review +--- +title: "[REVIVAL] {{module}} - Phase 2: PR triage" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 2](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-2-pr-triage-weeks-1-4-parallel-with-phase-1) +(decision tree, take-over and close templates) + +Goal: no PR sits without a decision. Runs alongside Phase 1. + +- [ ] No open PR is older than 30 days without a maintainer comment +- [ ] Every open PR is merged, closed with a note, or actively under review From 2cfaed73428218b5d253cb5f4b7db7e5d07b5d21 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:10:42 -0400 Subject: [PATCH 14/20] docs: point phase-3 reference links at existing playbook anchors --- docs/revival/phase-3.md | 102 ++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/docs/revival/phase-3.md b/docs/revival/phase-3.md index 2fdd4fe..bbb05b1 100644 --- a/docs/revival/phase-3.md +++ b/docs/revival/phase-3.md @@ -1,51 +1,51 @@ ---- -title: "[REVIVAL] {{module}} - Phase 3: Build modernization" -labels: revival -type: Task -role: Steward ---- - -Reference: [Playbook - Phase 3](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-3-build-modernization) -(standard stack, CI and release YAML) - -Goal: the repo builds with the standard stack and CI is green on all platforms. -Work in order; each step is its own PR. - -## 3a Pester - -- [ ] Tests exist and use Pester 5 -- [ ] `Invoke-Pester` passes locally - -## 3b psake - -- [ ] `psakeFile.ps1` defines `Init`, `Clean`, `Build`, `Test`, `Analyze`, `Publish` -- [ ] `Invoke-psake ?` lists them -- [ ] `Invoke-psake Test` passes - -## 3c PowerShellBuild - -- [ ] Build references PowerShellBuild for shared task logic -- [ ] `Test-ModuleManifest` passes -- [ ] `Invoke-psake Build` stages a clean module in `output/` - -## 3d PSScriptAnalyzer - -- [ ] `Invoke-psake Analyze` runs the org ruleset -- [ ] Zero warnings, or each suppression carries a justifying comment - -## 3e CI - -- [ ] `.github/workflows/ci.yml` calls the org reusable workflow -- [ ] CI green on `main` across Win/PS5.1, Win/PS7, Linux/PS7, macOS/PS7 -- [ ] Branch protection requires the CI check - -## 3f Release workflow - -- [ ] `.github/workflows/release.yml` calls the org reusable workflow -- [ ] A pre-release tag (`v*-beta.1`) ran the workflow end to end - -## 3g Coverage - -- [ ] Coverage report generated -- [ ] Baseline percentage recorded in the tracking issue -- [ ] Critical public functions have tests; gaps filed as issues +--- +title: "[REVIVAL] {{module}} - Phase 3: Build modernization" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 3](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-3-build-modernization-weeks-2-6) +(standard stack, CI and release YAML) + +Goal: the repo builds with the standard stack and CI is green on all platforms. +Work in order; each step is its own PR. + +## 3a Pester + +- [ ] Tests exist and use Pester 5 +- [ ] `Invoke-Pester` passes locally + +## 3b psake + +- [ ] `psakeFile.ps1` defines `Init`, `Clean`, `Build`, `Test`, `Analyze`, `Publish` +- [ ] `Invoke-psake ?` lists them +- [ ] `Invoke-psake Test` passes + +## 3c PowerShellBuild + +- [ ] Build references PowerShellBuild for shared task logic +- [ ] `Test-ModuleManifest` passes +- [ ] `Invoke-psake Build` stages a clean module in `output/` + +## 3d PSScriptAnalyzer + +- [ ] `Invoke-psake Analyze` runs the org ruleset +- [ ] Zero warnings, or each suppression carries a justifying comment + +## 3e CI + +- [ ] `.github/workflows/ci.yml` calls the org reusable workflow +- [ ] CI green on `main` across Win/PS5.1, Win/PS7, Linux/PS7, macOS/PS7 +- [ ] Branch protection requires the CI check + +## 3f Release workflow + +- [ ] `.github/workflows/release.yml` calls the org reusable workflow +- [ ] A pre-release tag (`v*-beta.1`) ran the workflow end to end + +## 3g Coverage + +- [ ] Coverage report generated +- [ ] Baseline percentage recorded in the tracking issue +- [ ] Critical public functions have tests; gaps filed as issues From e66c4c89e17e926f379ad55ad9cf4766b84bc64d Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:10:43 -0400 Subject: [PATCH 15/20] docs: point phase-4 reference links at existing playbook anchors --- docs/revival/phase-4.md | 54 ++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/revival/phase-4.md b/docs/revival/phase-4.md index 28f5ccc..c5bfdcf 100644 --- a/docs/revival/phase-4.md +++ b/docs/revival/phase-4.md @@ -1,27 +1,27 @@ ---- -title: "[REVIVAL] {{module}} - Phase 4: First clean release" -labels: revival -type: Task -role: Steward ---- - -Reference: [Playbook - Phase 4](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-4-first-clean-release) -(release steps) - -Goal: a release under the PowerShellOrg banner you are proud to put your name on. - -## Release gate - -- [ ] `CHANGELOG.md` covers everything since the last release -- [ ] Version bumped: patch / minor / major as the changes warrant -- [ ] CI green and PSScriptAnalyzer clean -- [ ] Manifest accurate: description, author or org, copyright, tags, URLs -- [ ] `README.md` current: install, working examples, badges -- [ ] Open critical bugs fixed or deferred with a written reason -- [ ] Release PR reviewed by one other maintainer (or Steward if solo) - -## Exit - -- [ ] Release on PSGallery under PowerShellOrg -- [ ] GitHub Release with human-readable notes -- [ ] PSGallery package description says PowerShellOrg +--- +title: "[REVIVAL] {{module}} - Phase 4: First clean release" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 4](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-4-first-clean-release-weeks-4-8) +(release steps) + +Goal: a release under the PowerShellOrg banner you are proud to put your name on. + +## Release gate + +- [ ] `CHANGELOG.md` covers everything since the last release +- [ ] Version bumped: patch / minor / major as the changes warrant +- [ ] CI green and PSScriptAnalyzer clean +- [ ] Manifest accurate: description, author or org, copyright, tags, URLs +- [ ] `README.md` current: install, working examples, badges +- [ ] Open critical bugs fixed or deferred with a written reason +- [ ] Release PR reviewed by one other maintainer (or Steward if solo) + +## Exit + +- [ ] Release on PSGallery under PowerShellOrg +- [ ] GitHub Release with human-readable notes +- [ ] PSGallery package description says PowerShellOrg From 9ae9bfa603810093d028b6adaffd598c7a2b0ec4 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:10:45 -0400 Subject: [PATCH 16/20] docs: point phase-5 reference links at existing playbook anchors --- docs/revival/phase-5.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/revival/phase-5.md b/docs/revival/phase-5.md index 6e37b5c..5d709c1 100644 --- a/docs/revival/phase-5.md +++ b/docs/revival/phase-5.md @@ -1,16 +1,16 @@ ---- -title: "[REVIVAL] {{module}} - Phase 5: Transition to active" -labels: revival -type: Task -role: Steward ---- - -Reference: [Playbook - Phase 5](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-5-ongoing-maintenance) -(cadence table, graduation criteria) - -Closing this closes the revival. Ongoing cadence and graduation are in the playbook. - -- [ ] Repo marked `status-active` (was `status-incoming`) -- [ ] `README.md` status badge updated -- [ ] Org Admin notified -- [ ] Announced in the Council channel +--- +title: "[REVIVAL] {{module}} - Phase 5: Transition to active" +labels: revival +type: Task +role: Steward +--- + +Reference: [Playbook - Phase 5](https://github.com/PowerShellOrg/.github/blob/main/docs/revival-playbook.md#phase-5-ongoing-maintenance-and-transition-to-active) +(cadence table, graduation criteria) + +Closing this closes the revival. Ongoing cadence and graduation are in the playbook. + +- [ ] Repo marked `status-active` (was `status-incoming`) +- [ ] `README.md` status badge updated +- [ ] Org Admin notified +- [ ] Announced in the Council channel From 382d0dd7933d70f83703d2f7bc346ea2b6fd11cf Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:50:04 -0400 Subject: [PATCH 17/20] ci: make revival-start a reusable workflow so issues land in the module repo --- .github/workflows/revival-start.yml | 61 ++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/.github/workflows/revival-start.yml b/.github/workflows/revival-start.yml index 93fa2b1..57a9e83 100644 --- a/.github/workflows/revival-start.yml +++ b/.github/workflows/revival-start.yml @@ -1,35 +1,68 @@ -name: Start a module revival +# Reusable workflow: start a module revival. +# +# Call it from the module repo so the issues land there, where contributors look. +# Drop this caller into the module repo as .github/workflows/revival-start.yml, then run it +# from the Actions tab (or `gh workflow run revival-start.yml -f steward=`): +# +# name: Start module revival +# on: +# workflow_dispatch: +# inputs: +# steward: +# description: "GitHub login of the Steward" +# required: false +# permissions: +# issues: write +# contents: read +# jobs: +# start: +# uses: PowerShellOrg/.github/.github/workflows/revival-start.yml@main +# with: +# steward: ${{ inputs.steward }} +# +# Opens one "[REVIVAL] " tracking issue plus six phase sub-issues from +# docs/revival/*.md in PowerShellOrg/.github. Idempotent: re-running finds the existing +# tracking issue and stops. Runs on the caller's GITHUB_TOKEN; no secrets. + +name: Start module revival (reusable) on: - workflow_dispatch: + workflow_call: inputs: module: - description: "Module name (e.g. BuildHelpers). Used in titles and, by default, as the repo name." - required: true + description: "Module name for titles. Defaults to the calling repo's name." + type: string + required: false steward: description: "GitHub login of the Steward (assigned to every issue)" + type: string required: false - repo: - description: "Repo to pull baseline metrics from, if not PowerShellOrg/" - required: false - adoption_issue: - description: "Adoption request issue number to comment on, if any" + source_ref: + description: "Branch or tag of PowerShellOrg/.github to read docs/revival from" + type: string required: false + default: main + outputs: + tracking_issue: + description: "Number of the tracking issue (new or existing)" + value: ${{ jobs.create.outputs.tracking_issue }} permissions: issues: write contents: read -concurrency: - group: revival-${{ inputs.module }} - cancel-in-progress: false - jobs: create: runs-on: ubuntu-latest + outputs: + tracking_issue: ${{ steps.run.outputs.tracking_issue }} steps: - uses: actions/checkout@v5 - - uses: actions/github-script@v8 + with: + repository: PowerShellOrg/.github + ref: ${{ inputs.source_ref }} + - id: run + uses: actions/github-script@v8 env: INPUTS: ${{ toJSON(inputs) }} with: From ba52352e6e71282965b2c86c170be492d1dd942c Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:50:06 -0400 Subject: [PATCH 18/20] feat: target the calling repo; drop adoption_issue input --- scripts/create-revival.js | 225 ++++++++++++++++++-------------------- 1 file changed, 107 insertions(+), 118 deletions(-) diff --git a/scripts/create-revival.js b/scripts/create-revival.js index b6791f2..b187540 100644 --- a/scripts/create-revival.js +++ b/scripts/create-revival.js @@ -1,118 +1,107 @@ -// Opens one [REVIVAL] tracking issue plus six phase sub-issues from docs/revival/*.md. -// Idempotent: re-running for the same module finds the existing tracking issue and stops. -// Runs under the default GITHUB_TOKEN (issues: write). Needs nothing else. -const fs = require('fs'); -const path = require('path'); - -const SOURCE_DIR = 'docs/revival'; -const PHASES = ['phase-0', 'phase-1', 'phase-2', 'phase-3', 'phase-4', 'phase-5']; -const TRACKING_LABEL = 'revival'; - -function parseSource(file) { - const raw = fs.readFileSync(path.join(process.cwd(), SOURCE_DIR, `${file}.md`), 'utf8'); - const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); - if (!m) throw new Error(`${file}.md has no frontmatter`); - const meta = {}; - for (const line of m[1].split(/\r?\n/)) { - const kv = line.match(/^(\w+):\s*(.*)$/); - if (kv) meta[kv[1]] = kv[2].replace(/^"(.*)"$/, '$1').trim(); - } - const labels = (meta.labels || '').split(',').map(s => s.trim()).filter(Boolean); - return { title: meta.title, type: meta.type || null, labels, body: raw.slice(m[0].length) }; -} - -function fill(text, vars) { - return text.replace(/\{\{(\w+)\}\}/g, (_, k) => (vars[k] ?? `_${k}_`)); -} - -async function baseline(github, fullName) { - const blank = { last_push: '_unknown_', open_issues: '_unknown_', open_prs: '_unknown_', stars: '_unknown_', forks: '_unknown_', ci: '_unknown_' }; - const [owner, repo] = fullName.split('/'); - let r; - try { r = (await github.rest.repos.get({ owner, repo })).data; } - catch (e) { if (e.status === 404) return { ...blank, repo_url: `https://github.com/${fullName}`, note: 'repo not found; fill baseline by hand' }; throw e; } - // open_issues_count includes PRs; subtract an exact PR count. No search API: it lags and misfired under GITHUB_TOKEN. - const open_prs = (await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 })).length; - const open_issues = r.open_issues_count - open_prs; - const ci = []; - for (const [p, name] of [['.github/workflows', 'GitHub Actions'], ['appveyor.yml', 'AppVeyor'], ['azure-pipelines.yml', 'Azure Pipelines'], ['.travis.yml', 'Travis']]) { - try { await github.rest.repos.getContent({ owner, repo, path: p }); ci.push(name); } catch (e) { if (e.status !== 404) throw e; } - } - return { - repo_url: r.html_url, last_push: r.pushed_at.slice(0, 10), open_issues, open_prs, - stars: r.stargazers_count, forks: r.forks_count, ci: ci.length ? ci.join(', ') : 'none', - }; -} - -async function findExisting(github, owner, repo, title) { - const issues = await github.paginate(github.rest.issues.listForRepo, { owner, repo, labels: TRACKING_LABEL, state: 'all', per_page: 100 }); - return issues.find(i => !i.pull_request && i.title === title) || null; -} - -async function ensureLabel(github, owner, repo) { - try { await github.rest.issues.getLabel({ owner, repo, name: TRACKING_LABEL }); } - catch (e) { - if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner, repo, name: TRACKING_LABEL, color: '5319e7', description: 'Module revival tracking' }); - } -} - -async function createIssue(github, owner, repo, src, vars) { - const params = { owner, repo, title: fill(src.title, vars), body: fill(src.body, vars), labels: src.labels }; - if (src.type) params.type = src.type; - if (vars.steward !== 'unassigned') params.assignees = [vars.steward.slice(1)]; - try { - return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; - } catch (e) { - // Issue type unknown to this org, or steward not assignable: retry bare rather than fail the run. - if (e.status !== 422) throw e; - console.warn(`422 creating "${params.title}" (${e.message}); retrying without type/assignees`); - delete params.type; delete params.assignees; - return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; - } -} - -module.exports = async function run({ github, context, core, inputs }) { - const { owner, repo } = context.repo; - const module = inputs.module.trim(); - const target = (inputs.repo || `${owner}/${module}`).trim(); - const tracking = parseSource('tracking'); - - const vars = { - module, steward: inputs.steward ? '@' + inputs.steward.trim().replace(/^@/, '') : 'unassigned', - today: new Date().toISOString().slice(0, 10), ...(await baseline(github, target)), - }; - const title = fill(tracking.title, vars); - - const existing = await findExisting(github, owner, repo, title); - if (existing) { - core.summary.addRaw(`Tracking issue already exists: [#${existing.number}](${existing.html_url}) (${existing.state}). Nothing created.`).write(); - core.setOutput('tracking_issue', existing.number); - return; - } - - await ensureLabel(github, owner, repo); - const parent = await createIssue(github, owner, repo, tracking, vars); - core.info(`Created ${parent.html_url}`); - - const children = []; - for (const name of PHASES) { - const child = await createIssue(github, owner, repo, parseSource(name), vars); - // sub_issue_id is the database id, not the issue number. - await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner, repo, issue_number: parent.number, sub_issue_id: child.id }); - children.push(child); - core.info(` + ${child.title} -> #${child.number}`); - } - - if (inputs.adoption_issue) { - await github.rest.issues.createComment({ owner, repo, issue_number: Number(inputs.adoption_issue), - body: `Revival tracking issue opened: #${parent.number}. Steward: ${vars.steward}.` }); - } - - core.setOutput('tracking_issue', parent.number); - core.summary.addHeading(`Revival started: ${module}`) - .addRaw(`Tracking issue [#${parent.number}](${parent.html_url}) with ${children.length} phase sub-issues.`) - .addList(children.map(c => `#${c.number} ${c.title}`)) - .addRaw(vars.note ? `\n\n> ${vars.note}` : '') - .write(); -}; +// Opens one [REVIVAL] tracking issue plus six phase sub-issues from docs/revival/*.md. +// Runs inside the reusable workflow, so context.repo is the CALLING module repo: issues are +// created there and the baseline is read from there. Idempotent: re-running for the same +// module finds the existing tracking issue and stops. Caller's GITHUB_TOKEN, issues: write. +const fs = require('fs'); +const path = require('path'); + +const SOURCE_DIR = 'docs/revival'; +const PHASES = ['phase-0', 'phase-1', 'phase-2', 'phase-3', 'phase-4', 'phase-5']; +const TRACKING_LABEL = 'revival'; + +function parseSource(file) { + const raw = fs.readFileSync(path.join(process.cwd(), SOURCE_DIR, `${file}.md`), 'utf8'); + const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); + if (!m) throw new Error(`${file}.md has no frontmatter`); + const meta = {}; + for (const line of m[1].split(/\r?\n/)) { + const kv = line.match(/^(\w+):\s*(.*)$/); + if (kv) meta[kv[1]] = kv[2].replace(/^"(.*)"$/, '$1').trim(); + } + const labels = (meta.labels || '').split(',').map(s => s.trim()).filter(Boolean); + return { title: meta.title, type: meta.type || null, labels, body: raw.slice(m[0].length) }; +} + +function fill(text, vars) { + return text.replace(/\{\{(\w+)\}\}/g, (_, k) => (vars[k] ?? `_${k}_`)); +} + +async function baseline(github, owner, repo) { + const r = (await github.rest.repos.get({ owner, repo })).data; + // open_issues_count includes PRs; subtract an exact PR count. + const open_prs = (await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 })).length; + const ci = []; + for (const [p, name] of [['.github/workflows', 'GitHub Actions'], ['appveyor.yml', 'AppVeyor'], ['azure-pipelines.yml', 'Azure Pipelines'], ['.travis.yml', 'Travis']]) { + try { await github.rest.repos.getContent({ owner, repo, path: p }); ci.push(name); } catch (e) { if (e.status !== 404) throw e; } + } + return { + repo_url: r.html_url, last_push: r.pushed_at.slice(0, 10), open_issues: r.open_issues_count - open_prs, open_prs, + stars: r.stargazers_count, forks: r.forks_count, ci: ci.length ? ci.join(', ') : 'none', + }; +} + +async function findExisting(github, owner, repo, title) { + const issues = await github.paginate(github.rest.issues.listForRepo, { owner, repo, labels: TRACKING_LABEL, state: 'all', per_page: 100 }); + return issues.find(i => !i.pull_request && i.title === title) || null; +} + +async function ensureLabel(github, owner, repo) { + try { await github.rest.issues.getLabel({ owner, repo, name: TRACKING_LABEL }); } + catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner, repo, name: TRACKING_LABEL, color: '5319e7', description: 'Module revival tracking' }); + } +} + +async function createIssue(github, owner, repo, src, vars) { + const params = { owner, repo, title: fill(src.title, vars), body: fill(src.body, vars), labels: src.labels }; + if (src.type) params.type = src.type; + if (vars.steward !== 'unassigned') params.assignees = [vars.steward.slice(1)]; + try { + return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; + } catch (e) { + // Issue type unknown to this org, or steward not assignable: retry bare rather than fail the run. + if (e.status !== 422) throw e; + console.warn(`422 creating "${params.title}" (${e.message}); retrying without type/assignees`); + delete params.type; delete params.assignees; + return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; + } +} + +module.exports = async function run({ github, context, core, inputs }) { + const { owner, repo } = context.repo; + const module = (inputs.module || repo).trim(); + const tracking = parseSource('tracking'); + + const vars = { + module, steward: inputs.steward ? '@' + inputs.steward.trim().replace(/^@/, '') : 'unassigned', + today: new Date().toISOString().slice(0, 10), ...(await baseline(github, owner, repo)), + }; + const title = fill(tracking.title, vars); + + const existing = await findExisting(github, owner, repo, title); + if (existing) { + core.summary.addRaw(`Tracking issue already exists: [#${existing.number}](${existing.html_url}) (${existing.state}). Nothing created.`).write(); + core.setOutput('tracking_issue', existing.number); + return; + } + + await ensureLabel(github, owner, repo); + const parent = await createIssue(github, owner, repo, tracking, vars); + core.info(`Created ${parent.html_url}`); + + const children = []; + for (const name of PHASES) { + const child = await createIssue(github, owner, repo, parseSource(name), vars); + // sub_issue_id is the database id, not the issue number. + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner, repo, issue_number: parent.number, sub_issue_id: child.id }); + children.push(child); + core.info(` + ${child.title} -> #${child.number}`); + } + + core.setOutput('tracking_issue', parent.number); + core.summary.addHeading(`Revival started: ${module}`) + .addRaw(`Tracking issue [#${parent.number}](${parent.html_url}) with ${children.length} phase sub-issues.`) + .addList(children.map(c => `#${c.number} ${c.title}`)) + .write(); +}; From 84747f96d6236d978395b0a885054a4b2dde2764 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:57:47 -0400 Subject: [PATCH 19/20] docs: add revival label to manifest; note first application --- docs/revival/labels.yml | 153 +++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/docs/revival/labels.yml b/docs/revival/labels.yml index 32d5eb4..35b7e6a 100644 --- a/docs/revival/labels.yml +++ b/docs/revival/labels.yml @@ -1,74 +1,79 @@ -# PowerShellOrg module-revival label manifest -# Destination: PowerShellOrg/.github (pending write access — see wayfinder T1) -# -# Convention: colon-space, lowercase. No emoji. No "/", "&", or apostrophes in names — -# they break label-sync tooling (vscode-powershell carries both "Area-Build & Release" -# and "Area-Build Release" from exactly this). -# -# One colour per facet. Only priority gets a gradient, and it lives in an Issue Field, -# not here. -# -# NOT in this file, deliberately: -# type: -> native org Issue Types (Task / Bug / Feature) — already enabled -# priority: -> org Issue Field "Priority" — Urgent / High / Medium / Low -# effort: -> dropped. Every human-estimated effort facet measured is dead or dying -# skill: -> dropped. Zero of 15 large repos surveyed track it - -# --------------------------------------------------------------------------- -# Discovery — EXACT strings GitHub indexes. Do not prefix, TitleCase, or emoji these. -# "good first issue" populates the repo Contribute page and feeds GitHub's -# approachability algorithm. Getting these wrong makes a revival invisible to the -# contributor funnel it exists to attract. -# --------------------------------------------------------------------------- -- name: "good first issue" - color: "7057ff" - description: "Newcomer-ready: solution explained, code identified, ready to test" -- name: "help wanted" - color: "008672" - description: "Maintainer welcomes a PR on this" - -# --------------------------------------------------------------------------- -# area: — what part of the project. Multi-valued. The four below are universal; -# the Steward adds module-specific ones (see the recipe in the playbook). -# --------------------------------------------------------------------------- -- name: "area: build" - color: "0052cc" - description: "CI, psake, packaging, release pipeline" -- name: "area: tests" - color: "0052cc" - description: "Pester tests and test infrastructure" -- name: "area: docs" - color: "0052cc" - description: "README, help text, examples, CONTRIBUTING" -- name: "area: triage" - color: "0052cc" - description: "Issue and PR triage, backlog cleanup" - -# --------------------------------------------------------------------------- -# status: — transient lifecycle state, often written by bots. Not a taxonomy. -# --------------------------------------------------------------------------- -- name: "status: needs-repro" - color: "bfc7d1" - description: "Awaiting reproduction steps or clarification from the reporter" -- name: "status: stale" - color: "bfc7d1" - description: "No recent activity; candidate for closing" - -# --------------------------------------------------------------------------- -# PRUNE — delete these on transfer. Each is superseded, not merely unfashionable. -# bug, enhancement -> native Issue Types (Bug, Feature) -# documentation -> area: docs -# duplicate -> native close reason "Duplicate" -# wontfix, invalid -> native close reason "Not planned" -# question -> GitHub Discussions (the playbook already routes these there) -# Prune only after checking usage; a label with real history may be worth migrating -# rather than deleting. -# --------------------------------------------------------------------------- -prune: - - "bug" - - "enhancement" - - "documentation" - - "duplicate" - - "wontfix" - - "invalid" - - "question" +# PowerShellOrg module-revival label manifest +# Lives at PowerShellOrg/.github/docs/revival/labels.yml (PR #16). First applied to BuildHelpers 2026-09-17. +# +# Convention: colon-space, lowercase. No emoji. No "/", "&", or apostrophes in names — +# they break label-sync tooling (vscode-powershell carries both "Area-Build & Release" +# and "Area-Build Release" from exactly this). +# +# One colour per facet. Only priority gets a gradient, and it lives in an Issue Field, +# not here. +# +# NOT in this file, deliberately: +# type: -> native org Issue Types (Task / Bug / Feature) — already enabled +# priority: -> org Issue Field "Priority" — Urgent / High / Medium / Low +# effort: -> dropped. Every human-estimated effort facet measured is dead or dying +# skill: -> dropped. Zero of 15 large repos surveyed track it + +# --------------------------------------------------------------------------- +# Discovery — EXACT strings GitHub indexes. Do not prefix, TitleCase, or emoji these. +# "good first issue" populates the repo Contribute page and feeds GitHub's +# approachability algorithm. Getting these wrong makes a revival invisible to the +# contributor funnel it exists to attract. +# --------------------------------------------------------------------------- +- name: "good first issue" + color: "7057ff" + description: "Newcomer-ready: solution explained, code identified, ready to test" +- name: "help wanted" + color: "008672" + description: "Maintainer welcomes a PR on this" + +# The revival-start workflow creates this if missing; listed so the colour matches. +- name: "revival" + color: "5319e7" + description: "Module revival tracking" + +# --------------------------------------------------------------------------- +# area: — what part of the project. Multi-valued. The four below are universal; +# the Steward adds module-specific ones (see the recipe in the playbook). +# --------------------------------------------------------------------------- +- name: "area: build" + color: "0052cc" + description: "CI, psake, packaging, release pipeline" +- name: "area: tests" + color: "0052cc" + description: "Pester tests and test infrastructure" +- name: "area: docs" + color: "0052cc" + description: "README, help text, examples, CONTRIBUTING" +- name: "area: triage" + color: "0052cc" + description: "Issue and PR triage, backlog cleanup" + +# --------------------------------------------------------------------------- +# status: — transient lifecycle state, often written by bots. Not a taxonomy. +# --------------------------------------------------------------------------- +- name: "status: needs-repro" + color: "bfc7d1" + description: "Awaiting reproduction steps or clarification from the reporter" +- name: "status: stale" + color: "bfc7d1" + description: "No recent activity; candidate for closing" + +# --------------------------------------------------------------------------- +# PRUNE — delete these on transfer. Each is superseded, not merely unfashionable. +# bug, enhancement -> native Issue Types (Bug, Feature) +# documentation -> area: docs +# duplicate -> native close reason "Duplicate" +# wontfix, invalid -> native close reason "Not planned" +# question -> GitHub Discussions (the playbook already routes these there) +# Prune only after checking usage; a label with real history may be worth migrating +# rather than deleting. +# --------------------------------------------------------------------------- +prune: + - "bug" + - "enhancement" + - "documentation" + - "duplicate" + - "wontfix" + - "invalid" + - "question" From 4b44af894dc447e6c314f38a3a205bc9c976e428 Mon Sep 17 00:00:00 2001 From: Trent Blackburn <45049539+tablackburn@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:50:40 -0400 Subject: [PATCH 20/20] fix: reconcile phases on re-run; exclude caller workflow from CI and last-commit baseline --- scripts/create-revival.js | 245 +++++++++++++++++++++----------------- 1 file changed, 138 insertions(+), 107 deletions(-) diff --git a/scripts/create-revival.js b/scripts/create-revival.js index b187540..998d064 100644 --- a/scripts/create-revival.js +++ b/scripts/create-revival.js @@ -1,107 +1,138 @@ -// Opens one [REVIVAL] tracking issue plus six phase sub-issues from docs/revival/*.md. -// Runs inside the reusable workflow, so context.repo is the CALLING module repo: issues are -// created there and the baseline is read from there. Idempotent: re-running for the same -// module finds the existing tracking issue and stops. Caller's GITHUB_TOKEN, issues: write. -const fs = require('fs'); -const path = require('path'); - -const SOURCE_DIR = 'docs/revival'; -const PHASES = ['phase-0', 'phase-1', 'phase-2', 'phase-3', 'phase-4', 'phase-5']; -const TRACKING_LABEL = 'revival'; - -function parseSource(file) { - const raw = fs.readFileSync(path.join(process.cwd(), SOURCE_DIR, `${file}.md`), 'utf8'); - const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); - if (!m) throw new Error(`${file}.md has no frontmatter`); - const meta = {}; - for (const line of m[1].split(/\r?\n/)) { - const kv = line.match(/^(\w+):\s*(.*)$/); - if (kv) meta[kv[1]] = kv[2].replace(/^"(.*)"$/, '$1').trim(); - } - const labels = (meta.labels || '').split(',').map(s => s.trim()).filter(Boolean); - return { title: meta.title, type: meta.type || null, labels, body: raw.slice(m[0].length) }; -} - -function fill(text, vars) { - return text.replace(/\{\{(\w+)\}\}/g, (_, k) => (vars[k] ?? `_${k}_`)); -} - -async function baseline(github, owner, repo) { - const r = (await github.rest.repos.get({ owner, repo })).data; - // open_issues_count includes PRs; subtract an exact PR count. - const open_prs = (await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 })).length; - const ci = []; - for (const [p, name] of [['.github/workflows', 'GitHub Actions'], ['appveyor.yml', 'AppVeyor'], ['azure-pipelines.yml', 'Azure Pipelines'], ['.travis.yml', 'Travis']]) { - try { await github.rest.repos.getContent({ owner, repo, path: p }); ci.push(name); } catch (e) { if (e.status !== 404) throw e; } - } - return { - repo_url: r.html_url, last_push: r.pushed_at.slice(0, 10), open_issues: r.open_issues_count - open_prs, open_prs, - stars: r.stargazers_count, forks: r.forks_count, ci: ci.length ? ci.join(', ') : 'none', - }; -} - -async function findExisting(github, owner, repo, title) { - const issues = await github.paginate(github.rest.issues.listForRepo, { owner, repo, labels: TRACKING_LABEL, state: 'all', per_page: 100 }); - return issues.find(i => !i.pull_request && i.title === title) || null; -} - -async function ensureLabel(github, owner, repo) { - try { await github.rest.issues.getLabel({ owner, repo, name: TRACKING_LABEL }); } - catch (e) { - if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner, repo, name: TRACKING_LABEL, color: '5319e7', description: 'Module revival tracking' }); - } -} - -async function createIssue(github, owner, repo, src, vars) { - const params = { owner, repo, title: fill(src.title, vars), body: fill(src.body, vars), labels: src.labels }; - if (src.type) params.type = src.type; - if (vars.steward !== 'unassigned') params.assignees = [vars.steward.slice(1)]; - try { - return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; - } catch (e) { - // Issue type unknown to this org, or steward not assignable: retry bare rather than fail the run. - if (e.status !== 422) throw e; - console.warn(`422 creating "${params.title}" (${e.message}); retrying without type/assignees`); - delete params.type; delete params.assignees; - return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; - } -} - -module.exports = async function run({ github, context, core, inputs }) { - const { owner, repo } = context.repo; - const module = (inputs.module || repo).trim(); - const tracking = parseSource('tracking'); - - const vars = { - module, steward: inputs.steward ? '@' + inputs.steward.trim().replace(/^@/, '') : 'unassigned', - today: new Date().toISOString().slice(0, 10), ...(await baseline(github, owner, repo)), - }; - const title = fill(tracking.title, vars); - - const existing = await findExisting(github, owner, repo, title); - if (existing) { - core.summary.addRaw(`Tracking issue already exists: [#${existing.number}](${existing.html_url}) (${existing.state}). Nothing created.`).write(); - core.setOutput('tracking_issue', existing.number); - return; - } - - await ensureLabel(github, owner, repo); - const parent = await createIssue(github, owner, repo, tracking, vars); - core.info(`Created ${parent.html_url}`); - - const children = []; - for (const name of PHASES) { - const child = await createIssue(github, owner, repo, parseSource(name), vars); - // sub_issue_id is the database id, not the issue number. - await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner, repo, issue_number: parent.number, sub_issue_id: child.id }); - children.push(child); - core.info(` + ${child.title} -> #${child.number}`); - } - - core.setOutput('tracking_issue', parent.number); - core.summary.addHeading(`Revival started: ${module}`) - .addRaw(`Tracking issue [#${parent.number}](${parent.html_url}) with ${children.length} phase sub-issues.`) - .addList(children.map(c => `#${c.number} ${c.title}`)) - .write(); -}; +// Opens one [REVIVAL] tracking issue plus six phase sub-issues from docs/revival/*.md. +// Runs inside the reusable workflow, so context.repo is the CALLING module repo: issues are +// created there and the baseline is read from there. Idempotent: re-running for the same +// module finds every existing issue by title, creates only what is missing, and attaches +// any phase not yet linked. Caller's GITHUB_TOKEN, issues: write. +const fs = require('fs'); +const path = require('path'); + +const SOURCE_DIR = 'docs/revival'; +const PHASES = ['phase-0', 'phase-1', 'phase-2', 'phase-3', 'phase-4', 'phase-5']; +const TRACKING_LABEL = 'revival'; + +function parseSource(file) { + const raw = fs.readFileSync(path.join(process.cwd(), SOURCE_DIR, `${file}.md`), 'utf8'); + const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); + if (!m) throw new Error(`${file}.md has no frontmatter`); + const meta = {}; + for (const line of m[1].split(/\r?\n/)) { + const kv = line.match(/^(\w+):\s*(.*)$/); + if (kv) meta[kv[1]] = kv[2].replace(/^"(.*)"$/, '$1').trim(); + } + const labels = (meta.labels || '').split(',').map(s => s.trim()).filter(Boolean); + return { title: meta.title, type: meta.type || null, labels, body: raw.slice(m[0].length) }; +} + +function fill(text, vars) { + return text.replace(/\{\{(\w+)\}\}/g, (_, k) => (vars[k] ?? `_${k}_`)); +} + +const CALLER_PATH = '.github/workflows/revival-start.yml'; + +async function baseline(github, owner, repo) { + const r = (await github.rest.repos.get({ owner, repo })).data; + // open_issues_count includes PRs; subtract an exact PR count. + const open_prs = (await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 })).length; + + // CI detection. The caller workflow lives in .github/workflows, so its presence alone is not CI. + const ci = []; + try { + const entries = (await github.rest.repos.getContent({ owner, repo, path: '.github/workflows' })).data; + if (Array.isArray(entries) && entries.some(e => `.github/workflows/${e.name}` !== CALLER_PATH && /\.ya?ml$/.test(e.name))) ci.push('GitHub Actions'); + } catch (e) { if (e.status !== 404) throw e; } + for (const [p, name] of [['appveyor.yml', 'AppVeyor'], ['azure-pipelines.yml', 'Azure Pipelines'], ['.travis.yml', 'Travis']]) { + try { await github.rest.repos.getContent({ owner, repo, path: p }); ci.push(name); } catch (e) { if (e.status !== 404) throw e; } + } + + // Last real commit: skip commits that only touch the caller file. + let last_push = r.pushed_at.slice(0, 10); + const recent = (await github.rest.repos.listCommits({ owner, repo, per_page: 5 })).data; + for (const c of recent) { + const files = (await github.rest.repos.getCommit({ owner, repo, ref: c.sha })).data.files || []; + if (files.length && files.every(f => f.filename === CALLER_PATH)) continue; + last_push = c.commit.committer.date.slice(0, 10); + break; + } + + return { + repo_url: r.html_url, last_push, open_issues: r.open_issues_count - open_prs, open_prs, + stars: r.stargazers_count, forks: r.forks_count, ci: ci.length ? ci.join(', ') : 'none', + }; +} + +async function revivalIssues(github, owner, repo) { + const issues = await github.paginate(github.rest.issues.listForRepo, { owner, repo, labels: TRACKING_LABEL, state: 'all', per_page: 100 }); + return new Map(issues.filter(i => !i.pull_request).map(i => [i.title, i])); +} + +async function subIssueIds(github, owner, repo, parentNumber) { + const subs = await github.paginate('GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner, repo, issue_number: parentNumber, per_page: 100 }); + return new Set(subs.map(s => s.id)); +} + +async function ensureLabel(github, owner, repo) { + try { await github.rest.issues.getLabel({ owner, repo, name: TRACKING_LABEL }); } + catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner, repo, name: TRACKING_LABEL, color: '5319e7', description: 'Module revival tracking' }); + } +} + +async function createIssue(github, owner, repo, src, vars) { + const params = { owner, repo, title: fill(src.title, vars), body: fill(src.body, vars), labels: src.labels }; + if (src.type) params.type = src.type; + if (vars.steward !== 'unassigned') params.assignees = [vars.steward.slice(1)]; + try { + return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; + } catch (e) { + // Issue type unknown to this org, or steward not assignable: retry bare rather than fail the run. + if (e.status !== 422) throw e; + console.warn(`422 creating "${params.title}" (${e.message}); retrying without type/assignees`); + delete params.type; delete params.assignees; + return (await github.request('POST /repos/{owner}/{repo}/issues', params)).data; + } +} + +module.exports = async function run({ github, context, core, inputs }) { + const { owner, repo } = context.repo; + const module = (inputs.module || repo).trim(); + const tracking = parseSource('tracking'); + + const vars = { + module, steward: inputs.steward ? '@' + inputs.steward.trim().replace(/^@/, '') : 'unassigned', + today: new Date().toISOString().slice(0, 10), ...(await baseline(github, owner, repo)), + }; + await ensureLabel(github, owner, repo); + const byTitle = await revivalIssues(github, owner, repo); + const findOrCreate = async (src) => { + const title = fill(src.title, vars); + if (byTitle.has(title)) { core.info(`exists ${title} -> #${byTitle.get(title).number}`); return { issue: byTitle.get(title), created: false }; } + const issue = await createIssue(github, owner, repo, src, vars); + byTitle.set(title, issue); + core.info(`created ${title} -> #${issue.number}`); + return { issue, created: true }; + }; + + // Find-or-create the parent, then reconcile every phase and every attachment. A re-run after a + // partial failure completes the set instead of stopping at "parent exists". + const { issue: parent, created: parentCreated } = await findOrCreate(tracking); + const attached = await subIssueIds(github, owner, repo, parent.number); + const children = []; let createdCount = parentCreated ? 1 : 0, attachedCount = 0; + for (const name of PHASES) { + const { issue: child, created } = await findOrCreate(parseSource(name)); + if (created) createdCount++; + if (!attached.has(child.id)) { + // sub_issue_id is the database id, not the issue number. + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner, repo, issue_number: parent.number, sub_issue_id: child.id }); + attachedCount++; + } + children.push(child); + } + + const verb = createdCount === 0 && attachedCount === 0 ? 'Already complete' : `Reconciled (${createdCount} created, ${attachedCount} attached)`; + core.setOutput('tracking_issue', parent.number); + core.summary.addHeading(`Revival: ${module}`) + .addRaw(`${verb}. Tracking issue [#${parent.number}](${parent.html_url}) with ${children.length} phase sub-issues.`) + .addList(children.map(c => `#${c.number} ${c.title}`)) + .write(); +};