From 06d4850bd5fbd6e4ee3981161d587b863665e96e Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 20 Sep 2026 00:05:17 -0600 Subject: [PATCH] ci: remove completed retired image package cleanup workflow --- .../workflows/delete-retired-sms-package.yml | 181 ------------------ 1 file changed, 181 deletions(-) delete mode 100644 .github/workflows/delete-retired-sms-package.yml diff --git a/.github/workflows/delete-retired-sms-package.yml b/.github/workflows/delete-retired-sms-package.yml deleted file mode 100644 index 108646a..0000000 --- a/.github/workflows/delete-retired-sms-package.yml +++ /dev/null @@ -1,181 +0,0 @@ ---- -# TEMPORARY owner-approved cleanup workflow. Deletes the retired container -# package ghcr.io/makeitworkcloud/opencode-sms-bridge (image source removed -# in d833bb6). Manual workflow_dispatch only, main only, exact repo only. -# Remove this workflow after the confirmed cleanup run. -# -# Known limits, deliberately not bypassed: -# - The org-wide package inventory GET returned 400 Invalid argument for the -# repository GITHUB_TOKEN, so only the exact package-scoped endpoints are -# used; an initial metadata 404 FAILS the run (it may mask a permission -# problem) instead of claiming the package is absent. -# - GitHub refuses REST deletion of a public package when any version has -# more than 5000 downloads; the run fails and GitHub support is the path. -# - GITHUB_TOKEN deletion works because this repository's publishing -# workflow created the package (publisher repo holds admin by default). -# - A deleted package stays restorable for 30 days via the packages API. -name: delete-retired-sms-package - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -concurrency: - group: ${{ github.repository }}-delete-retired-sms-package - cancel-in-progress: false - -jobs: - delete: - if: github.repository == 'makeitworkcloud/images' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Verify and delete package opencode-sms-bridge - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const owner = 'makeitworkcloud'; - const expectedRepo = 'makeitworkcloud/images'; - const expectedName = 'opencode-sms-bridge'; - const packageType = 'container'; - const maxVerifyAttempts = 5; - - // Fail with status and message only: never log raw error - // objects, stack traces, or request headers. - const failWith = (label, error) => { - const status = error && Number.isInteger(error.status) ? error.status : 'no-status'; - const message = error && error.message ? String(error.message) : 'unknown error'; - core.setFailed(`${label}: status=${status} message=${message}`); - }; - - // Fail-closed guard mirroring the job-level condition. - const repoFull = `${context.repo.owner}/${context.repo.repo}`; - if (repoFull !== expectedRepo || context.ref !== 'refs/heads/main') { - core.setFailed(`refusing: ${repoFull}@${context.ref} is not ${expectedRepo}@refs/heads/main`); - return; - } - - // 0) Pre-request self-check BEFORE any API call: structured REST - // methods encode route parameters themselves (octokit - // endpoint.js url-template applies encodeURIComponent to - // simple {name} placeholders), so the raw name must build a - // URL with the slashed name encoded exactly once. - const route = github.request.endpoint( - 'GET /orgs/{org}/packages/{package_type}/{package_name}', - { org: owner, package_type: packageType, package_name: expectedName }, - ); - const expectedPath = `/orgs/${owner}/packages/${packageType}/${encodeURIComponent(expectedName)}`; - const builtPath = new URL(route.url).pathname; - if (builtPath !== expectedPath) { - core.setFailed(`route encoding check failed: ${builtPath} != ${expectedPath}`); - return; - } - - // 1) Exact metadata GET with the publisher GITHUB_TOKEN. The - // org-wide inventory endpoint is NOT used (it returns 400 - // Invalid argument for this token). Any error, including - // 404, FAILS: 404 may mask a permission problem, so absence - // is never inferred here. - let meta; - try { - meta = ( - await github.rest.packages.getPackageForOrganization({ - org: owner, - package_type: packageType, - package_name: expectedName, - }) - ).data; - } catch (error) { - failWith('package metadata GET failed', error); - return; - } - const repoLabel = meta.repository ? meta.repository.full_name : 'none'; - core.info(`resolved id=${meta.id} name=${meta.name} type=${meta.package_type}`); - core.info(`visibility=${meta.visibility} version_count=${meta.version_count} repository=${repoLabel}`); - const problems = []; - if (!Number.isSafeInteger(meta.id) || meta.id <= 0) { - problems.push(`id ${meta.id} is not a positive safe integer`); - } - if (meta.name !== expectedName) problems.push(`name "${meta.name}" != "${expectedName}"`); - if (meta.package_type !== packageType) problems.push(`type "${meta.package_type}" != "${packageType}"`); - if (!meta.repository) problems.push('package has no repository association'); - else if (meta.repository.full_name !== expectedRepo) { - problems.push(`repository association ${meta.repository.full_name} != ${expectedRepo}`); - } - if (problems.length > 0) { - core.setFailed(`verification failed, refusing to delete: ${problems.join('; ')}`); - return; - } - - // 2) Count real versions from the exact endpoint before deleting. - let versions; - try { - versions = await github.paginate( - github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg, - { org: owner, package_type: packageType, package_name: expectedName, per_page: 100 }, - ); - } catch (error) { - failWith('package versions GET failed', error); - return; - } - core.info(`versions present: ${versions.length} (sample id=${versions.length > 0 ? versions[0].id : 'none'})`); - - // 3) Delete the ENTIRE package, not individual versions, via the - // documented REST route. The pinned runtime exposes no - // github.rest.packages helper for this endpoint (run - // 35492786770 failed "is not a function"), so call - // github.request with raw parameters; the runtime encodes - // them and step 0 already verified this exact route's URL - // construction. Require the documented 204 response before - // treating the deletion as accepted. - let deleteResponse; - try { - deleteResponse = await github.request( - 'DELETE /orgs/{org}/packages/{package_type}/{package_name}', - { org: owner, package_type: packageType, package_name: expectedName }, - ); - } catch (error) { - failWith('package DELETE failed', error); - return; - } - if (deleteResponse.status !== 204) { - core.setFailed(`package DELETE returned status=${deleteResponse.status}, expected 204`); - return; - } - core.info(`DELETE accepted (204) for "${expectedName}" (id=${meta.id})`); - - // 4) Bounded verification with the same token: at most five - // metadata GETs; 200 waits 1s and retries (eventual - // consistency), 404 confirms the accepted deletion, any - // other result fails. No org-wide relist is used (the - // inventory endpoint 400s for this token). - for (let attempt = 1; attempt <= maxVerifyAttempts; attempt += 1) { - try { - await github.rest.packages.getPackageForOrganization({ - org: owner, - package_type: packageType, - package_name: expectedName, - }); - } catch (error) { - if (error && error.status === 404) { - core.notice( - `deleted package "${expectedName}" (id=${meta.id}, ${versions.length} version(s)); ` + - `post-delete GET returned 404 on attempt ${attempt}`, - ); - return; - } - failWith(`post-delete GET failed on attempt ${attempt}`, error); - return; - } - if (attempt < maxVerifyAttempts) { - core.info(`post-delete GET attempt ${attempt} still returned 200; waiting 1s for eventual consistency`); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } - core.setFailed( - `package "${expectedName}" (id=${meta.id}) was still readable after DELETE (204) ` + - `and ${maxVerifyAttempts} verification attempts`, - );